refactor(ai): deduplicate JSON, record, and URL query helpers (#57604)

This commit is contained in:
Aiden Cline
2026-10-20 22:39:32 -05:00
committed by GitHub
Unverified
parent 51626eee90
commit cc996eece1
6 changed files with 21 additions and 35 deletions
+2 -4
View File
@@ -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))))
}
+3 -7
View File
@@ -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 {
@@ -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. */
@@ -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,
@@ -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 {
@@ -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")
+3 -5
View File
@@ -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,
@@ -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",
@@ -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
}
@@ -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")
@@ -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) }),
+5 -11
View File
@@ -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) }
@@ -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,