Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions packages/ai/src/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,17 @@ export type RequestInput<SelectedLanguageModel extends LanguageModel = LanguageM
}

export function generate<const Model extends LanguageModel>(
input: RequestInput<Model>,
input: RequestInput<Model> | LLMRequest,
options?: StreamOptions,
): Effect.Effect<LLMResponse, AIError, Service>
export function generate(input: LLMRequest, options?: StreamOptions): Effect.Effect<LLMResponse, AIError, Service>
export function generate(input: RequestInput | LLMRequest, options?: StreamOptions) {
return requestEffect(input).pipe(Effect.flatMap((request) => LLMClient.generate(request, options)))
}

export function stream<const Model extends LanguageModel>(
input: RequestInput<Model>,
input: RequestInput<Model> | LLMRequest,
options?: StreamOptions,
): Stream.Stream<LLMEvent, AIError, Service>
export function stream(input: LLMRequest, options?: StreamOptions): Stream.Stream<LLMEvent, AIError, Service>
export function stream(input: RequestInput | LLMRequest, options?: StreamOptions) {
return Stream.unwrap(requestEffect(input).pipe(Effect.map((request) => LLMClient.stream(request, options))))
}
Expand Down
10 changes: 3 additions & 7 deletions packages/ai/src/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import type { AwaitOptions, Event, Generation, Snapshot } from "./generation.js"
import { Image, type ImageModel, type ImageRequest, type ImageRequestInput } from "./image.js"
import { LLM } from "./index.js"
import { Media } from "./media.js"
import { tryRequest } from "./media-model.js"
import { RequestExecutor } from "./route/executor.js"
import { AIError, InvalidRequestError, LanguageModel, LLMRequest } from "./schema/index.js"
import { AIError, InvalidRequestError, LanguageModel, type LLMRequest } from "./schema/index.js"
import type { RequestInput } from "./llm.js"
import { Speech, type SpeechModel, type SpeechRequest, type SpeechRequestInput } from "./speech.js"
import {
Expand Down Expand Up @@ -93,9 +92,6 @@ export const make = (options: Options = {}) => {
cancel: (options) => run(generation.cancel(), options),
})

const llmRequest = (input: RequestInput | LLMRequest) =>
input instanceof LLMRequest ? Effect.succeed(input) : tryRequest(() => LLM.request(input))

return {
run,
/** Decoded asset bytes, downloading `url` sources through the executor. */
Expand Down Expand Up @@ -137,9 +133,9 @@ export const make = (options: Options = {}) => {
llm: {
request: LLM.request,
generate: <const Model extends LanguageModel>(input: RequestInput<Model> | LLMRequest, options?: RunOptions) =>
run(Effect.flatMap(llmRequest(input), LLM.generate), options),
run(LLM.generate(input), options),
stream: <const Model extends LanguageModel>(input: RequestInput<Model> | LLMRequest, options?: RunOptions) =>
iterate(Stream.unwrap(Effect.map(llmRequest(input), LLM.stream)), options),
iterate(LLM.stream(input), options),
},
image: {
request: Image.request,
Expand Down
9 changes: 5 additions & 4 deletions packages/ai/src/protocols/utils/partial-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@
*/

import { Schema } from "effect"
import { decodeJson } from "../../utils/json.js"
import { Allow } from "./partial-json-options.js"
export * from "./partial-json-options.js"

export class PartialJSON extends Error {}
export class MalformedJSON extends Error {}

const decodeJson = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown))
const decodeJsonString = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.String))

/** Parse complete or incomplete JSON, restricted by the supplied partial-value flags. */
export function parseJSON(jsonString: string, allowPartial = Allow.ALL): unknown {
Expand Down Expand Up @@ -175,16 +176,16 @@ const _parseJSON = (jsonString: string, allow: number) => {
}
if (jsonString.charAt(index) === '"') {
try {
return decodeJson(jsonString.substring(start, ++index - Number(escape))) as string
return decodeJsonString(jsonString.substring(start, ++index - Number(escape)))
} catch (error) {
throwMalformedError(String(error))
}
}
if (Allow.STR & allow) {
try {
return decodeJson(`${jsonString.substring(start, index - Number(escape))}"`) as string
return decodeJsonString(`${jsonString.substring(start, index - Number(escape))}"`)
} catch {
return decodeJson(`${jsonString.substring(start, jsonString.lastIndexOf("\\"))}"`) as string
return decodeJsonString(`${jsonString.substring(start, jsonString.lastIndexOf("\\"))}"`)
}
}
return markPartialJSON("Unterminated string literal")
Expand Down
8 changes: 3 additions & 5 deletions packages/ai/src/provider-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
type HttpContext,
type HttpRateLimitDetails,
} from "./schema/index.js"
import { Json } from "./utils/json.js"
import { isRecord } from "./utils/record.js"

const patterns = [
/prompt is too long/i,
Expand Down Expand Up @@ -115,7 +117,7 @@ export const isRetryable = (error: AIError) => {
}
}

const decodeJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown))
const decodeJson = Schema.decodeUnknownOption(Json)
// OpenCode Zen reports account caps as typed 429/402 errors that are not throttles.
const QUOTA_CODES = new Set([
"insufficient_quota",
Expand Down Expand Up @@ -333,7 +335,3 @@ function providerCodes(value: unknown) {
exception?.type,
].filter((value): value is string => typeof value === "string")
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null
}
7 changes: 3 additions & 4 deletions packages/ai/src/providers/cloudflare-ai-gateway.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Schema, type Config, type Redacted } from "effect"
import type { Config, Redacted } from "effect"
import type { ProviderPackage } from "../provider-package.js"
import { AnthropicMessages, OpenAIChat, OpenAIResponses } from "../protocols/index.js"
import { Auth } from "../route/auth.js"
import type { AtLeastOne, ProviderAuthOption } from "../route/auth-options.js"
import type { RouteDefaultsInput } from "../route/client.js"
import { ProviderConfigurationError, ProviderID, type ModelID } from "../schema/index.js"
import { encodeJson } from "../utils/json.js"
import type { OpenAIProviderOptionsInput } from "./openai-options.js"

export const id = ProviderID.make("cloudflare-ai-gateway")
Expand Down Expand Up @@ -86,9 +87,7 @@ const credential = (input: LanguageModelOptions) =>
.orElse(Auth.config(authEnvVars[1]))

const headers = (input: LanguageModelOptions) => ({
...(input.metadata === undefined
? {}
: { "cf-aig-metadata": Schema.encodeSync(Schema.fromJsonString(Schema.Unknown))(input.metadata) }),
...(input.metadata === undefined ? {} : { "cf-aig-metadata": encodeJson(input.metadata) }),
...(input.cacheTtl === undefined ? {} : { "cf-aig-cache-ttl": String(input.cacheTtl) }),
...(input.cacheKey === undefined ? {} : { "cf-aig-cache-key": input.cacheKey }),
...(input.skipCache === undefined ? {} : { "cf-aig-skip-cache": String(input.skipCache) }),
Expand Down
16 changes: 5 additions & 11 deletions packages/ai/src/route/transport/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,6 @@ export interface HttpPrepared<Frame> {
readonly middleware?: HttpMiddleware
}

const applyQuery = (url: string, query: Record<string, string> | undefined) => {
if (!query) return url
const next = new URL(url)
Object.entries(query).forEach(([key, value]) => next.searchParams.set(key, value))
return next.toString()
}

const bodyWithOverlay = <Body>(body: Body, request: LLMRequest, encodeBody: (body: Body) => string) =>
Effect.gen(function* () {
if (request.http?.body === undefined) return { jsonBody: body, bodyText: encodeBody(body) }
Expand All @@ -51,10 +44,11 @@ const bodyWithOverlay = <Body>(body: Body, request: LLMRequest, encodeBody: (bod

export const jsonRequestParts = <Body>(input: JsonRequestInput<Body>) =>
Effect.gen(function* () {
const url = applyQuery(
renderEndpoint(input.endpoint, { request: input.request, body: input.body }).toString(),
input.request.http?.query,
)
const endpoint = renderEndpoint(input.endpoint, { request: input.request, body: input.body })
if (input.request.http?.query) {
Object.entries(input.request.http.query).forEach(([key, value]) => endpoint.searchParams.set(key, value))
}
const url = endpoint.toString()
const body = yield* bodyWithOverlay(input.body, input.request, input.encodeBody)
const headers = yield* Auth.toEffect(input.auth)({
request: input.request,
Expand Down
Loading