refactor(ai): colocate provider options (#38698)

This commit is contained in:
Shoubhit Dash 2026-07-24 19:08:36 +05:30 committed by GitHub
commit 0374d29232
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 127 additions and 149 deletions

View file

@ -13,6 +13,7 @@ import {
type JsonSchema,
type LLMRequest,
type MediaPart,
type ProviderOptions,
type ProviderMetadata,
type ToolCallPart,
type ToolDefinition,
@ -22,7 +23,6 @@ import {
import { JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared"
import { classifyProviderFailure } from "../provider-error"
import * as Cache from "./utils/cache"
import { AnthropicOptions } from "./utils/anthropic-options"
import { Lifecycle } from "./utils/lifecycle"
import { ToolSchemaProjection } from "./utils/tool-schema"
import { ToolStream } from "./utils/tool-stream"
@ -32,6 +32,29 @@ const MEDIA_MIMES = new Set<string>([...ProviderShared.IMAGE_MIMES, ...ProviderS
export const DEFAULT_BASE_URL = "https://api.anthropic.com/v1"
export const PATH = "/messages"
export type ThinkingInput =
| {
readonly type: "adaptive"
readonly display?: "summarized" | "omitted"
}
| {
readonly type: "disabled"
}
| ({ readonly type: "enabled" } & (
| { readonly budgetTokens: number; readonly budget_tokens?: number }
| { readonly budgetTokens?: number; readonly budget_tokens: number }
))
export interface OptionsInput {
readonly [key: string]: unknown
readonly thinking?: ThinkingInput
readonly effort?: string
}
export type ProviderOptionsInput = ProviderOptions & {
readonly anthropic?: OptionsInput
}
// =============================================================================
// Request Body Schema
// =============================================================================
@ -174,6 +197,20 @@ const AnthropicToolChoice = Schema.Union([
Schema.Struct({ type: Schema.tag("tool"), name: Schema.String }),
])
const AnthropicThinking = Schema.Union([
Schema.Struct({
type: Schema.tag("enabled"),
budget_tokens: Schema.Number,
}),
Schema.Struct({
type: Schema.tag("adaptive"),
display: Schema.optional(Schema.Literals(["summarized", "omitted"])),
}),
Schema.Struct({
type: Schema.tag("disabled"),
}),
])
const AnthropicOutputConfig = Schema.Struct({
effort: Schema.optional(Schema.String),
})
@ -190,7 +227,7 @@ const AnthropicBodyFields = {
top_p: Schema.optional(Schema.Number),
top_k: Schema.optional(Schema.Number),
stop_sequences: optionalArray(Schema.String),
thinking: Schema.optional(AnthropicOptions.ThinkingSchema),
thinking: Schema.optional(AnthropicThinking),
output_config: Schema.optional(AnthropicOutputConfig),
}
export const AnthropicMessagesBody = Schema.Struct(AnthropicBodyFields)
@ -524,6 +561,38 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
return messages
})
const resolveOptions = Effect.fn("AnthropicMessages.resolveOptions")(function* (request: LLMRequest) {
const input = request.providerOptions?.anthropic
return {
thinking: yield* resolveThinking(input?.thinking),
effort: typeof input?.effort === "string" ? input.effort : undefined,
}
})
const resolveThinking = Effect.fn("AnthropicMessages.resolveThinking")(function* (input: unknown) {
if (!ProviderShared.isRecord(input)) return undefined
if (input.type === "adaptive") {
const display =
input.display === "summarized"
? ("summarized" as const)
: input.display === "omitted"
? ("omitted" as const)
: undefined
return { type: "adaptive" as const, ...(display === undefined ? {} : { display }) }
}
if (input.type === "disabled") return { type: "disabled" as const }
if (input.type !== "enabled") return undefined
const budget =
typeof input.budgetTokens === "number"
? input.budgetTokens
: typeof input.budget_tokens === "number"
? input.budget_tokens
: undefined
if (budget === undefined)
return yield* ProviderShared.invalidRequest("Anthropic thinking provider option requires budgetTokens")
return { type: "enabled" as const, budget_tokens: budget }
})
const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) {
const generation = request.generation
const toolSchemaCompatibility = request.model.compatibility?.toolSchema
@ -558,7 +627,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
`Anthropic Messages: dropped ${breakpoints.dropped} cache breakpoint(s); the API allows at most ${ANTHROPIC_BREAKPOINT_CAP} per request.`,
)
}
const options = yield* AnthropicOptions.resolve(request)
const options = yield* resolveOptions(request)
return {
model: request.model.id,
system,

View file

@ -11,6 +11,7 @@ import {
type JsonSchema,
type LLMRequest,
type MediaPart,
type ProviderOptions,
type ProviderMetadata,
type TextPart,
type ToolCallPart,
@ -18,7 +19,6 @@ import {
type ToolContent,
} from "../schema"
import { JsonObject, optionalArray, ProviderShared } from "./shared"
import { GeminiOptions } from "./utils/gemini-options"
import { GeminiToolSchema } from "./utils/gemini-tool-schema"
import { Lifecycle } from "./utils/lifecycle"
import { ToolSchemaProjection } from "./utils/tool-schema"
@ -27,6 +27,18 @@ const ADAPTER = "gemini"
const MEDIA_MIMES = new Set<string>(ProviderShared.MEDIA_MIMES)
export const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
export interface OptionsInput {
readonly [key: string]: unknown
readonly thinkingConfig?: {
readonly thinkingBudget?: number
readonly includeThoughts?: boolean
}
}
export type ProviderOptionsInput = ProviderOptions & {
readonly gemini?: OptionsInput
}
// =============================================================================
// Request Body Schema
// =============================================================================
@ -96,13 +108,18 @@ const GeminiToolConfig = Schema.Struct({
}),
})
const GeminiThinkingConfig = Schema.Struct({
thinkingBudget: Schema.optional(Schema.Number),
includeThoughts: Schema.optional(Schema.Boolean),
})
const GeminiGenerationConfig = Schema.Struct({
maxOutputTokens: Schema.optional(Schema.Number),
temperature: Schema.optional(Schema.Number),
topP: Schema.optional(Schema.Number),
topK: Schema.optional(Schema.Number),
stopSequences: optionalArray(Schema.String),
thinkingConfig: Schema.optional(GeminiOptions.ThinkingConfigSchema),
thinkingConfig: Schema.optional(GeminiThinkingConfig),
})
const GeminiBodyFields = {
@ -298,10 +315,22 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR
return contents
})
const resolveOptions = (request: LLMRequest) => {
const value = request.providerOptions?.gemini?.thinkingConfig
if (!ProviderShared.isRecord(value)) return {}
const thinkingConfig = {
thinkingBudget: typeof value.thinkingBudget === "number" ? value.thinkingBudget : undefined,
includeThoughts: typeof value.includeThoughts === "boolean" ? value.includeThoughts : undefined,
}
return {
thinkingConfig: Object.values(thinkingConfig).some((item) => item !== undefined) ? thinkingConfig : undefined,
}
}
const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMRequest) {
const hasTools = request.tools.length > 0
const generation = request.generation
const options = GeminiOptions.resolve(request)
const options = resolveOptions(request)
const toolSchemaCompatibility = request.model.compatibility?.toolSchema
const generationConfig = {
maxOutputTokens: generation?.maxTokens,

View file

@ -1,57 +0,0 @@
import { Effect, Schema } from "effect"
import type { LLMRequest } from "../../schema"
import { ProviderShared } from "../shared"
export const ThinkingSchema = Schema.Union([
Schema.Struct({
type: Schema.tag("enabled"),
budget_tokens: Schema.Number,
}),
Schema.Struct({
type: Schema.tag("adaptive"),
display: Schema.optional(Schema.Literals(["summarized", "omitted"])),
}),
Schema.Struct({
type: Schema.tag("disabled"),
}),
])
export type Thinking = Schema.Schema.Type<typeof ThinkingSchema>
export interface Resolved {
readonly thinking?: Thinking
readonly effort?: string
}
export const resolve = Effect.fn("AnthropicOptions.resolve")(function* (request: LLMRequest) {
const input = request.providerOptions?.anthropic
return {
thinking: yield* resolveThinking(input?.thinking),
effort: typeof input?.effort === "string" ? input.effort : undefined,
} satisfies Resolved
})
const resolveThinking = Effect.fn("AnthropicOptions.resolveThinking")(function* (input: unknown) {
if (!ProviderShared.isRecord(input)) return undefined
if (input.type === "adaptive") {
const display =
input.display === "summarized"
? ("summarized" as const)
: input.display === "omitted"
? ("omitted" as const)
: undefined
return { type: "adaptive" as const, ...(display === undefined ? {} : { display }) }
}
if (input.type === "disabled") return { type: "disabled" as const }
if (input.type !== "enabled") return undefined
const budget =
typeof input.budgetTokens === "number"
? input.budgetTokens
: typeof input.budget_tokens === "number"
? input.budget_tokens
: undefined
if (budget === undefined)
return yield* ProviderShared.invalidRequest("Anthropic thinking provider option requires budgetTokens")
return { type: "enabled" as const, budget_tokens: budget }
})
export * as AnthropicOptions from "./anthropic-options"

View file

@ -1,27 +0,0 @@
import { Schema } from "effect"
import type { LLMRequest } from "../../schema"
import { ProviderShared } from "../shared"
export const ThinkingConfigSchema = Schema.Struct({
thinkingBudget: Schema.optional(Schema.Number),
includeThoughts: Schema.optional(Schema.Boolean),
})
export type ThinkingConfig = Schema.Schema.Type<typeof ThinkingConfigSchema>
export interface Resolved {
readonly thinkingConfig?: ThinkingConfig
}
export const resolve = (request: LLMRequest): Resolved => {
const value = request.providerOptions?.gemini?.thinkingConfig
if (!ProviderShared.isRecord(value)) return {}
const thinkingConfig = {
thinkingBudget: typeof value.thinkingBudget === "number" ? value.thinkingBudget : undefined,
includeThoughts: typeof value.includeThoughts === "boolean" ? value.includeThoughts : undefined,
}
return {
thinkingConfig: Object.values(thinkingConfig).some((item) => item !== undefined) ? thinkingConfig : undefined,
}
}
export * as GeminiOptions from "./gemini-options"

View file

@ -4,9 +4,10 @@ import { Auth } from "../route/auth"
import type { ProviderAuthOption } from "../route/auth-options"
import type { RouteDefaultsInput } from "../route/client"
import { ProviderID, type ModelID } from "../schema"
import type { AnthropicProviderOptionsInput } from "./anthropic-options"
export type { AnthropicOptionsInput, AnthropicProviderOptionsInput, AnthropicThinkingInput } from "./anthropic-options"
export type AnthropicOptionsInput = AnthropicMessages.OptionsInput
export type AnthropicProviderOptionsInput = AnthropicMessages.ProviderOptionsInput
export type AnthropicThinkingInput = AnthropicMessages.ThinkingInput
export const id = ProviderID.make("anthropic-compatible")
@ -14,7 +15,7 @@ export type Config = RouteDefaultsInput &
ProviderAuthOption<"optional"> & {
readonly provider?: string
readonly baseURL: string
readonly providerOptions?: AnthropicProviderOptionsInput
readonly providerOptions?: AnthropicMessages.ProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
@ -24,7 +25,7 @@ export type Settings = ProviderPackage.Settings &
) & {
readonly baseURL: string
readonly provider?: string
readonly providerOptions?: AnthropicProviderOptionsInput
readonly providerOptions?: AnthropicMessages.ProviderOptionsInput
}
export const routes = [AnthropicMessages.route]

View file

@ -1,26 +0,0 @@
import type { ProviderOptions } from "../schema"
export type AnthropicThinkingInput =
| {
readonly type: "adaptive"
readonly display?: "summarized" | "omitted"
}
| {
readonly type: "disabled"
}
| ({ readonly type: "enabled" } & (
| { readonly budgetTokens: number; readonly budget_tokens?: number }
| { readonly budgetTokens?: number; readonly budget_tokens: number }
))
export interface AnthropicOptionsInput {
readonly [key: string]: unknown
readonly thinking?: AnthropicThinkingInput
readonly effort?: string
}
export type AnthropicProviderOptionsInput = ProviderOptions & {
readonly anthropic?: AnthropicOptionsInput
}
export * as AnthropicProviderOptions from "./anthropic-options"

View file

@ -5,9 +5,10 @@ import type { ProviderPackage } from "../provider-package"
import { ProviderID, type ModelID } from "../schema"
import { AnthropicMessages } from "../protocols/anthropic-messages"
import { AnthropicCompatible } from "./anthropic-compatible"
import type { AnthropicProviderOptionsInput } from "./anthropic-options"
export type { AnthropicOptionsInput, AnthropicProviderOptionsInput, AnthropicThinkingInput } from "./anthropic-options"
export type AnthropicOptionsInput = AnthropicMessages.OptionsInput
export type AnthropicProviderOptionsInput = AnthropicMessages.ProviderOptionsInput
export type AnthropicThinkingInput = AnthropicMessages.ThinkingInput
export const id = ProviderID.make("anthropic")
@ -16,7 +17,7 @@ export const routes = [AnthropicMessages.route]
export type Config = RouteDefaultsInput &
ProviderAuthOption<"optional"> & {
readonly baseURL?: string
readonly providerOptions?: AnthropicProviderOptionsInput
readonly providerOptions?: AnthropicMessages.ProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
@ -25,7 +26,7 @@ export type Settings = ProviderPackage.Settings &
| { readonly apiKey?: never; readonly authToken?: string }
) & {
readonly baseURL?: string
readonly providerOptions?: AnthropicProviderOptionsInput
readonly providerOptions?: AnthropicMessages.ProviderOptionsInput
}
const auth = (options: ProviderAuthOption<"optional">) => {

View file

@ -1,13 +0,0 @@
import type { ThinkingConfig } from "../protocols/utils/gemini-options"
import type { ProviderOptions } from "../schema"
export interface GeminiOptionsInput {
readonly [key: string]: unknown
readonly thinkingConfig?: ThinkingConfig
}
export type GeminiProviderOptionsInput = ProviderOptions & {
readonly gemini?: GeminiOptionsInput
}
export * as GeminiProviderOptions from "./gemini-options"

View file

@ -7,10 +7,11 @@ import { Endpoint } from "../route/endpoint"
import { Framing } from "../route/framing"
import { Protocol } from "../route/protocol"
import { ProviderID, type ModelID } from "../schema"
import type { AnthropicProviderOptionsInput } from "./anthropic-options"
import { GoogleVertexShared } from "./google-vertex-shared"
export type { AnthropicOptionsInput, AnthropicProviderOptionsInput, AnthropicThinkingInput } from "./anthropic-options"
export type AnthropicOptionsInput = AnthropicMessages.OptionsInput
export type AnthropicProviderOptionsInput = AnthropicMessages.ProviderOptionsInput
export type AnthropicThinkingInput = AnthropicMessages.ThinkingInput
const VERSION = "vertex-2023-10-16" as const
@ -22,7 +23,7 @@ export type Config = RouteDefaultsInput &
readonly baseURL?: string
readonly location?: string
readonly project?: string
readonly providerOptions?: AnthropicProviderOptionsInput
readonly providerOptions?: AnthropicMessages.ProviderOptionsInput
}
export interface Settings extends ProviderPackage.Settings {
@ -31,7 +32,7 @@ export interface Settings extends ProviderPackage.Settings {
readonly baseURL?: string
readonly location?: string
readonly project?: string
readonly providerOptions?: AnthropicProviderOptionsInput
readonly providerOptions?: AnthropicMessages.ProviderOptionsInput
}
const route = Route.make({

View file

@ -5,10 +5,10 @@ import { Route, type RouteDefaultsInput } from "../route/client"
import { Endpoint } from "../route/endpoint"
import { Framing } from "../route/framing"
import { ProviderID, type ModelID } from "../schema"
import type { GeminiProviderOptionsInput } from "./gemini-options"
import { GoogleVertexShared } from "./google-vertex-shared"
export type { GeminiOptionsInput, GeminiProviderOptionsInput } from "./gemini-options"
export type GeminiOptionsInput = Gemini.OptionsInput
export type GeminiProviderOptionsInput = Gemini.ProviderOptionsInput
export const id = ProviderID.make("google-vertex")
@ -17,7 +17,7 @@ export type Config = RouteDefaultsInput &
readonly baseURL?: string
readonly location?: string
readonly project?: string
readonly providerOptions?: GeminiProviderOptionsInput
readonly providerOptions?: Gemini.ProviderOptionsInput
}
export type Settings = ProviderPackage.Settings &
@ -28,7 +28,7 @@ export type Settings = ProviderPackage.Settings &
readonly baseURL?: string
readonly location?: string
readonly project?: string
readonly providerOptions?: GeminiProviderOptionsInput
readonly providerOptions?: Gemini.ProviderOptionsInput
}
const route = Route.make({

View file

@ -5,10 +5,10 @@ import type { ProviderPackage } from "../provider-package"
import { HttpOptions, ProviderID, mergeHttpOptions, type ModelID } from "../schema"
import { Gemini } from "../protocols/gemini"
import { GoogleImages } from "../protocols/google-images"
import type { GeminiProviderOptionsInput } from "./gemini-options"
export type { GoogleImageOptions } from "../protocols/google-images"
export type { GeminiOptionsInput, GeminiProviderOptionsInput } from "./gemini-options"
export type GeminiOptionsInput = Gemini.OptionsInput
export type GeminiProviderOptionsInput = Gemini.ProviderOptionsInput
export const id = ProviderID.make("google")
@ -17,13 +17,13 @@ export const routes = [Gemini.route]
export type Config = RouteDefaultsInput &
ProviderAuthOption<"optional"> & {
readonly baseURL?: string
readonly providerOptions?: GeminiProviderOptionsInput
readonly providerOptions?: Gemini.ProviderOptionsInput
}
export interface Settings extends ProviderPackage.Settings {
readonly apiKey?: string
readonly baseURL?: string
readonly providerOptions?: GeminiProviderOptionsInput
readonly providerOptions?: Gemini.ProviderOptionsInput
}
const auth = (options: ProviderAuthOption<"optional">) => {