refactor(ai): normalize provider option parsing (#38695)
This commit is contained in:
parent
c06186a9d9
commit
d90da82be2
14 changed files with 256 additions and 84 deletions
|
|
@ -22,6 +22,7 @@ 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"
|
||||
|
|
@ -173,20 +174,6 @@ 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),
|
||||
})
|
||||
|
|
@ -203,7 +190,7 @@ const AnthropicBodyFields = {
|
|||
top_p: Schema.optional(Schema.Number),
|
||||
top_k: Schema.optional(Schema.Number),
|
||||
stop_sequences: optionalArray(Schema.String),
|
||||
thinking: Schema.optional(AnthropicThinking),
|
||||
thinking: Schema.optional(AnthropicOptions.ThinkingSchema),
|
||||
output_config: Schema.optional(AnthropicOutputConfig),
|
||||
}
|
||||
export const AnthropicMessagesBody = Schema.Struct(AnthropicBodyFields)
|
||||
|
|
@ -537,37 +524,6 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
|
|||
return messages
|
||||
})
|
||||
|
||||
const anthropicOptions = (request: LLMRequest) => request.providerOptions?.anthropic
|
||||
|
||||
const lowerThinking = Effect.fn("AnthropicMessages.lowerThinking")(function* (request: LLMRequest) {
|
||||
const thinking = anthropicOptions(request)?.thinking
|
||||
if (!ProviderShared.isRecord(thinking)) return undefined
|
||||
if (thinking.type === "adaptive") {
|
||||
const display =
|
||||
thinking.display === "summarized"
|
||||
? ("summarized" as const)
|
||||
: thinking.display === "omitted"
|
||||
? ("omitted" as const)
|
||||
: undefined
|
||||
return { type: "adaptive" as const, ...(display === undefined ? {} : { display }) }
|
||||
}
|
||||
if (thinking.type === "disabled") return { type: "disabled" as const }
|
||||
if (thinking.type !== "enabled") return undefined
|
||||
const budget =
|
||||
typeof thinking.budgetTokens === "number"
|
||||
? thinking.budgetTokens
|
||||
: typeof thinking.budget_tokens === "number"
|
||||
? thinking.budget_tokens
|
||||
: undefined
|
||||
if (budget === undefined) return yield* invalid("Anthropic thinking provider option requires budgetTokens")
|
||||
return { type: "enabled" as const, budget_tokens: budget }
|
||||
})
|
||||
|
||||
const outputConfig = (request: LLMRequest) => {
|
||||
const effort = anthropicOptions(request)?.effort
|
||||
return typeof effort === "string" ? { effort } : undefined
|
||||
}
|
||||
|
||||
const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (request: LLMRequest) {
|
||||
const generation = request.generation
|
||||
const toolSchemaCompatibility = request.model.compatibility?.toolSchema
|
||||
|
|
@ -587,8 +543,7 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
|
|||
),
|
||||
)
|
||||
// Anthropic rejects tool_choice when tools are absent; "none" is only meaningful with tools present.
|
||||
const toolChoice =
|
||||
tools === undefined || !request.toolChoice ? undefined : yield* lowerToolChoice(request.toolChoice)
|
||||
const toolChoice = tools === undefined || !request.toolChoice ? undefined : yield* lowerToolChoice(request.toolChoice)
|
||||
const system =
|
||||
request.system.length === 0
|
||||
? undefined
|
||||
|
|
@ -603,6 +558,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)
|
||||
return {
|
||||
model: request.model.id,
|
||||
system,
|
||||
|
|
@ -615,8 +571,8 @@ const fromRequest = Effect.fn("AnthropicMessages.fromRequest")(function* (reques
|
|||
top_p: generation?.topP,
|
||||
top_k: generation?.topK,
|
||||
stop_sequences: generation?.stop,
|
||||
thinking: yield* lowerThinking(request),
|
||||
output_config: outputConfig(request),
|
||||
thinking: options.thinking,
|
||||
output_config: options.effort === undefined ? undefined : { effort: options.effort },
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ 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"
|
||||
|
|
@ -95,18 +96,13 @@ 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(GeminiThinkingConfig),
|
||||
thinkingConfig: Schema.optional(GeminiOptions.ThinkingConfigSchema),
|
||||
})
|
||||
|
||||
const GeminiBodyFields = {
|
||||
|
|
@ -203,7 +199,9 @@ const thoughtSignature = (providerMetadata: ProviderMetadata | undefined) => {
|
|||
|
||||
const functionCallId = (providerMetadata: ProviderMetadata | undefined) => {
|
||||
const google = providerMetadata?.google
|
||||
return ProviderShared.isRecord(google) && typeof google.functionCallId === "string" ? google.functionCallId : undefined
|
||||
return ProviderShared.isRecord(google) && typeof google.functionCallId === "string"
|
||||
? google.functionCallId
|
||||
: undefined
|
||||
}
|
||||
|
||||
const lowerToolCall = (part: ToolCallPart) => ({
|
||||
|
|
@ -300,21 +298,10 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR
|
|||
return contents
|
||||
})
|
||||
|
||||
const geminiOptions = (request: LLMRequest) => request.providerOptions?.gemini
|
||||
|
||||
const thinkingConfig = (request: LLMRequest) => {
|
||||
const value = geminiOptions(request)?.thinkingConfig
|
||||
if (!ProviderShared.isRecord(value)) return undefined
|
||||
const result = {
|
||||
thinkingBudget: typeof value.thinkingBudget === "number" ? value.thinkingBudget : undefined,
|
||||
includeThoughts: typeof value.includeThoughts === "boolean" ? value.includeThoughts : undefined,
|
||||
}
|
||||
return Object.values(result).some((item) => item !== undefined) ? result : 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 toolSchemaCompatibility = request.model.compatibility?.toolSchema
|
||||
const generationConfig = {
|
||||
maxOutputTokens: generation?.maxTokens,
|
||||
|
|
@ -322,7 +309,7 @@ const fromRequest = Effect.fn("Gemini.fromRequest")(function* (request: LLMReque
|
|||
topP: generation?.topP,
|
||||
topK: generation?.topK,
|
||||
stopSequences: generation?.stop,
|
||||
thinkingConfig: thinkingConfig(request),
|
||||
thinkingConfig: options.thinkingConfig,
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
57
packages/ai/src/protocols/utils/anthropic-options.ts
Normal file
57
packages/ai/src/protocols/utils/anthropic-options.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
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"
|
||||
27
packages/ai/src/protocols/utils/gemini-options.ts
Normal file
27
packages/ai/src/protocols/utils/gemini-options.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
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"
|
||||
|
|
@ -3,7 +3,10 @@ import { AnthropicMessages } from "../protocols/anthropic-messages"
|
|||
import { Auth } from "../route/auth"
|
||||
import type { ProviderAuthOption } from "../route/auth-options"
|
||||
import type { RouteDefaultsInput } from "../route/client"
|
||||
import { ProviderID, type ModelID, type ProviderOptions } from "../schema"
|
||||
import { ProviderID, type ModelID } from "../schema"
|
||||
import type { AnthropicProviderOptionsInput } from "./anthropic-options"
|
||||
|
||||
export type { AnthropicOptionsInput, AnthropicProviderOptionsInput, AnthropicThinkingInput } from "./anthropic-options"
|
||||
|
||||
export const id = ProviderID.make("anthropic-compatible")
|
||||
|
||||
|
|
@ -11,6 +14,7 @@ export type Config = RouteDefaultsInput &
|
|||
ProviderAuthOption<"optional"> & {
|
||||
readonly provider?: string
|
||||
readonly baseURL: string
|
||||
readonly providerOptions?: AnthropicProviderOptionsInput
|
||||
}
|
||||
|
||||
export type Settings = ProviderPackage.Settings &
|
||||
|
|
@ -20,7 +24,7 @@ export type Settings = ProviderPackage.Settings &
|
|||
) & {
|
||||
readonly baseURL: string
|
||||
readonly provider?: string
|
||||
readonly providerOptions?: ProviderOptions
|
||||
readonly providerOptions?: AnthropicProviderOptionsInput
|
||||
}
|
||||
|
||||
export const routes = [AnthropicMessages.route]
|
||||
|
|
|
|||
26
packages/ai/src/providers/anthropic-options.ts
Normal file
26
packages/ai/src/providers/anthropic-options.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
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"
|
||||
|
|
@ -2,15 +2,22 @@ import type { RouteDefaultsInput } from "../route/client"
|
|||
import { Auth } from "../route/auth"
|
||||
import type { ProviderAuthOption } from "../route/auth-options"
|
||||
import type { ProviderPackage } from "../provider-package"
|
||||
import { ProviderID, type ModelID, type ProviderOptions } from "../schema"
|
||||
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 const id = ProviderID.make("anthropic")
|
||||
|
||||
export const routes = [AnthropicMessages.route]
|
||||
|
||||
export type Config = RouteDefaultsInput & ProviderAuthOption<"optional"> & { readonly baseURL?: string }
|
||||
export type Config = RouteDefaultsInput &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: AnthropicProviderOptionsInput
|
||||
}
|
||||
|
||||
export type Settings = ProviderPackage.Settings &
|
||||
(
|
||||
|
|
@ -18,7 +25,7 @@ export type Settings = ProviderPackage.Settings &
|
|||
| { readonly apiKey?: never; readonly authToken?: string }
|
||||
) & {
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: ProviderOptions
|
||||
readonly providerOptions?: AnthropicProviderOptionsInput
|
||||
}
|
||||
|
||||
const auth = (options: ProviderAuthOption<"optional">) => {
|
||||
|
|
|
|||
13
packages/ai/src/providers/gemini-options.ts
Normal file
13
packages/ai/src/providers/gemini-options.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
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"
|
||||
|
|
@ -6,9 +6,12 @@ import { Route, type RouteDefaultsInput } from "../route/client"
|
|||
import { Endpoint } from "../route/endpoint"
|
||||
import { Framing } from "../route/framing"
|
||||
import { Protocol } from "../route/protocol"
|
||||
import { ProviderID, type ModelID, type ProviderOptions } from "../schema"
|
||||
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"
|
||||
|
||||
const VERSION = "vertex-2023-10-16" as const
|
||||
|
||||
// models.dev uses this provider id even though the API contract is Anthropic Messages.
|
||||
|
|
@ -19,6 +22,7 @@ export type Config = RouteDefaultsInput &
|
|||
readonly baseURL?: string
|
||||
readonly location?: string
|
||||
readonly project?: string
|
||||
readonly providerOptions?: AnthropicProviderOptionsInput
|
||||
}
|
||||
|
||||
export interface Settings extends ProviderPackage.Settings {
|
||||
|
|
@ -27,7 +31,7 @@ export interface Settings extends ProviderPackage.Settings {
|
|||
readonly baseURL?: string
|
||||
readonly location?: string
|
||||
readonly project?: string
|
||||
readonly providerOptions?: ProviderOptions
|
||||
readonly providerOptions?: AnthropicProviderOptionsInput
|
||||
}
|
||||
|
||||
const route = Route.make({
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { Auth } from "../route/auth"
|
|||
import { Route, type RouteDefaultsInput } from "../route/client"
|
||||
import { Endpoint } from "../route/endpoint"
|
||||
import { Framing } from "../route/framing"
|
||||
import { ProviderID, type ModelID, type ProviderOptions } from "../schema"
|
||||
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 const id = ProviderID.make("google-vertex")
|
||||
|
||||
export type Config = RouteDefaultsInput &
|
||||
|
|
@ -14,6 +17,7 @@ export type Config = RouteDefaultsInput &
|
|||
readonly baseURL?: string
|
||||
readonly location?: string
|
||||
readonly project?: string
|
||||
readonly providerOptions?: GeminiProviderOptionsInput
|
||||
}
|
||||
|
||||
export type Settings = ProviderPackage.Settings &
|
||||
|
|
@ -24,7 +28,7 @@ export type Settings = ProviderPackage.Settings &
|
|||
readonly baseURL?: string
|
||||
readonly location?: string
|
||||
readonly project?: string
|
||||
readonly providerOptions?: ProviderOptions
|
||||
readonly providerOptions?: GeminiProviderOptionsInput
|
||||
}
|
||||
|
||||
const route = Route.make({
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ import type { RouteDefaultsInput } from "../route/client"
|
|||
import { Auth } from "../route/auth"
|
||||
import type { ProviderAuthOption } from "../route/auth-options"
|
||||
import type { ProviderPackage } from "../provider-package"
|
||||
import { HttpOptions, ProviderID, mergeHttpOptions, type ModelID, type ProviderOptions } from "../schema"
|
||||
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 const id = ProviderID.make("google")
|
||||
|
||||
|
|
@ -15,12 +17,13 @@ export const routes = [Gemini.route]
|
|||
export type Config = RouteDefaultsInput &
|
||||
ProviderAuthOption<"optional"> & {
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: GeminiProviderOptionsInput
|
||||
}
|
||||
|
||||
export interface Settings extends ProviderPackage.Settings {
|
||||
readonly apiKey?: string
|
||||
readonly baseURL?: string
|
||||
readonly providerOptions?: ProviderOptions
|
||||
readonly providerOptions?: GeminiProviderOptionsInput
|
||||
}
|
||||
|
||||
const auth = (options: ProviderAuthOption<"optional">) => {
|
||||
|
|
|
|||
|
|
@ -137,15 +137,26 @@ Azure.configure({ apiKey: "azure-key", resourceName: "resource" }).chat("deploym
|
|||
Azure.configure({ resourceName: "resource", apiKey: "azure-key", auth: Auth.header("api-key", "override") })
|
||||
|
||||
Anthropic.configure({ apiKey: "anthropic-key" }).model("claude-haiku")
|
||||
Anthropic.configure({
|
||||
apiKey: "anthropic-key",
|
||||
providerOptions: {
|
||||
anthropic: { thinking: { type: "enabled", budgetTokens: 1_024 }, effort: "high" },
|
||||
},
|
||||
}).model("claude-haiku")
|
||||
// @ts-expect-error Anthropic model selectors only accept model ids.
|
||||
Anthropic.configure({ apiKey: "anthropic-key" }).model("claude-haiku", {})
|
||||
// @ts-expect-error Anthropic package settings accept only one auth source.
|
||||
Anthropic.model("claude-sonnet-4-6", { apiKey: "anthropic-key", authToken: "anthropic-token" })
|
||||
// @ts-expect-error Enabled Anthropic thinking requires a token budget.
|
||||
Anthropic.configure({ providerOptions: { anthropic: { thinking: { type: "enabled" } } } })
|
||||
// @ts-expect-error Anthropic thinking budgets must be numbers.
|
||||
Anthropic.configure({ providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: "large" } } } })
|
||||
|
||||
AnthropicCompatible.configure({
|
||||
apiKey: "messages-key",
|
||||
baseURL: "https://messages.example.com/v1",
|
||||
provider: "example",
|
||||
providerOptions: { anthropic: { thinking: { type: "disabled" } } },
|
||||
}).model("compatible-model")
|
||||
// @ts-expect-error Anthropic-compatible providers require a base URL.
|
||||
AnthropicCompatible.configure({ apiKey: "messages-key" })
|
||||
|
|
@ -159,10 +170,19 @@ AnthropicCompatible.model("compatible-model", {
|
|||
})
|
||||
|
||||
Google.configure({ apiKey: "google-key" }).model("gemini-2.5-flash")
|
||||
Google.configure({
|
||||
apiKey: "google-key",
|
||||
providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 0, includeThoughts: false } } },
|
||||
}).model("gemini-2.5-flash")
|
||||
// @ts-expect-error Google model selectors only accept model ids.
|
||||
Google.configure({ apiKey: "google-key" }).model("gemini-2.5-flash", {})
|
||||
// @ts-expect-error Gemini thinking budgets must be numbers.
|
||||
Google.configure({ providerOptions: { gemini: { thinkingConfig: { thinkingBudget: "large" } } } })
|
||||
|
||||
GoogleVertex.configure({ apiKey: "vertex-key" }).model("gemini-3.5-flash")
|
||||
GoogleVertex.configure({
|
||||
apiKey: "vertex-key",
|
||||
providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 1_024 } } },
|
||||
}).model("gemini-3.5-flash")
|
||||
GoogleVertex.configure({ accessToken: "vertex-token", project: "project" }).model("gemini-3.5-flash")
|
||||
GoogleVertex.configure({ auth: Auth.bearer("vertex-token"), project: "project" }).model("gemini-3.5-flash")
|
||||
// @ts-expect-error Vertex Gemini model selectors only accept model ids.
|
||||
|
|
@ -208,7 +228,11 @@ GoogleVertexResponses.configure({
|
|||
project: "project",
|
||||
})
|
||||
|
||||
GoogleVertexMessages.configure({ accessToken: "vertex-token", project: "project" }).model("claude-sonnet-4-6")
|
||||
GoogleVertexMessages.configure({
|
||||
accessToken: "vertex-token",
|
||||
project: "project",
|
||||
providerOptions: { anthropic: { thinking: { type: "adaptive", display: "omitted" }, effort: "low" } },
|
||||
}).model("claude-sonnet-4-6")
|
||||
// @ts-expect-error Vertex Messages package settings do not accept API keys.
|
||||
GoogleVertexMessages.model("claude-sonnet-4-6", { apiKey: "vertex-key", project: "project" })
|
||||
GoogleVertexMessages.configure({ auth: Auth.bearer("vertex-token"), project: "project" }).model("claude-sonnet-4-6")
|
||||
|
|
|
|||
|
|
@ -74,6 +74,42 @@ describe("Anthropic Messages route", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("normalizes enabled and disabled thinking settings", () =>
|
||||
Effect.gen(function* () {
|
||||
const enabled = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.updateRequest(request, {
|
||||
providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 1_024 } } },
|
||||
}),
|
||||
)
|
||||
const legacy = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.updateRequest(request, {
|
||||
providerOptions: { anthropic: { thinking: { type: "enabled", budget_tokens: 2_048 } } },
|
||||
}),
|
||||
)
|
||||
const disabled = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
LLM.updateRequest(request, {
|
||||
providerOptions: { anthropic: { thinking: { type: "disabled" } } },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(enabled.body.thinking).toEqual({ type: "enabled", budget_tokens: 1_024 })
|
||||
expect(legacy.body.thinking).toEqual({ type: "enabled", budget_tokens: 2_048 })
|
||||
expect(disabled.body.thinking).toEqual({ type: "disabled" })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects enabled thinking without a budget", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* LLMClient.prepare(
|
||||
LLM.updateRequest(request, {
|
||||
providerOptions: { anthropic: { thinking: { type: "enabled" } } },
|
||||
}),
|
||||
).pipe(Effect.flip)
|
||||
|
||||
expect(error.message).toContain("Anthropic thinking provider option requires budgetTokens")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("lowers chronological system updates natively for Claude Opus 4.8 with cache hints", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
|
||||
|
|
@ -993,7 +1029,10 @@ describe("Anthropic Messages route", () => {
|
|||
content: [
|
||||
{ type: "text", text: "What is in this image?" },
|
||||
{ type: "image", source: { type: "base64", media_type: "image/png", data: "AAECAw==" } },
|
||||
{ type: "document", source: { type: "base64", media_type: "application/pdf", data: "JVBERi0xLjQ=" } },
|
||||
{
|
||||
type: "document",
|
||||
source: { type: "base64", media_type: "application/pdf", data: "JVBERi0xLjQ=" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -36,6 +36,27 @@ describe("Gemini route", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("normalizes Gemini thinking options", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<Gemini.GeminiBody>(
|
||||
LLM.updateRequest(request, {
|
||||
providerOptions: { gemini: { thinkingConfig: { thinkingBudget: 0, includeThoughts: false } } },
|
||||
}),
|
||||
)
|
||||
const filtered = yield* LLMClient.prepare<Gemini.GeminiBody>(
|
||||
LLM.updateRequest(request, {
|
||||
providerOptions: { gemini: { thinkingConfig: { thinkingBudget: "invalid", includeThoughts: false } } },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.generationConfig?.thinkingConfig).toEqual({
|
||||
thinkingBudget: 0,
|
||||
includeThoughts: false,
|
||||
})
|
||||
expect(filtered.body.generationConfig?.thinkingConfig).toEqual({ includeThoughts: false })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("lowers chronological system updates to wrapped user text in order", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* LLMClient.prepare<Gemini.GeminiBody>(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue