mirror of
https://github.com/anomalyco/opencode.git
synced 2026-10-22 09:59:53 +00:00
refactor(ai): reuse JsonObject, ProviderMetadata, and responseHttp across protocols and gateways (#57615)
This commit is contained in:
parent
678e49d828
commit
6843fb7b9c
8 changed files with 47 additions and 45 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { Clock, Effect, Redacted, Schema } from "effect"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { RequestExecutor } from "../route/executor.js"
|
||||
import { AIError, HttpContext, InvalidProviderOutputError, InvalidRequestError } from "../schema/index.js"
|
||||
import { AIError, InvalidProviderOutputError, InvalidRequestError } from "../schema/index.js"
|
||||
import { encodeJson } from "../utils/json.js"
|
||||
|
||||
const OPENAI_BASE_URL = "https://api.openai.com/v1"
|
||||
|
|
@ -157,7 +157,7 @@ const post = <A>(
|
|||
: HttpClientRequest.bodyText(encodeJson(body), "application/json"),
|
||||
),
|
||||
)
|
||||
const http = new HttpContext({ url: res.request.url, status: res.status, headers: res.headers })
|
||||
const http = RequestExecutor.responseHttp(res)
|
||||
const fail = (message: string, cause?: unknown, raw?: string) =>
|
||||
new AIError({ reason: new InvalidProviderOutputError({ route, message, body: raw, http, cause }) })
|
||||
const text = yield* res.text.pipe(Effect.mapError((cause) => fail(`Failed to read ${route} response`, cause)))
|
||||
|
|
|
|||
|
|
@ -11,13 +11,15 @@ import {
|
|||
type EvaluationOptions,
|
||||
} from "./evaluation.js"
|
||||
import { Auth, type Definition as AuthDefinition } from "../route/auth.js"
|
||||
import { RequestExecutor } from "../route/executor.js"
|
||||
import { JsonObject, ProviderShared } from "../protocols/shared.js"
|
||||
import {
|
||||
AIError,
|
||||
HttpContext,
|
||||
HttpOptions,
|
||||
InvalidProviderOutputError,
|
||||
InvalidRequestError,
|
||||
ModelID,
|
||||
ProviderMetadata,
|
||||
Usage,
|
||||
mergeJsonRecords,
|
||||
} from "../schema/index.js"
|
||||
|
|
@ -51,7 +53,7 @@ const Request = Schema.StructWithRest(
|
|||
state: EvaluationInput,
|
||||
questions: Schema.Record(Schema.String, Question),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Any)],
|
||||
[JsonObject],
|
||||
)
|
||||
|
||||
const Probability = Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 1 }))
|
||||
|
|
@ -75,7 +77,7 @@ const NativeUsage = Schema.StructWithRest(
|
|||
input_tokens: Schema.optional(Schema.Number),
|
||||
output_tokens: Schema.optional(Schema.Number),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
[JsonObject],
|
||||
)
|
||||
const Response = Schema.Struct({
|
||||
model: Schema.String,
|
||||
|
|
@ -83,7 +85,7 @@ const Response = Schema.Struct({
|
|||
usage: Schema.optional(NativeUsage),
|
||||
id: Schema.optional(Schema.String),
|
||||
provider: Schema.optional(Schema.String),
|
||||
provider_metadata: Schema.optional(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown))),
|
||||
provider_metadata: Schema.optional(ProviderMetadata),
|
||||
})
|
||||
|
||||
export interface ModelInput {
|
||||
|
|
@ -132,7 +134,7 @@ export const model = <Options extends EvaluationOptions = EvaluationOptions>(cfg
|
|||
HttpClientRequest.bodyText(body, "application/json"),
|
||||
),
|
||||
)
|
||||
const http = new HttpContext({ url: res.request.url, status: res.status, headers: res.headers })
|
||||
const http = RequestExecutor.responseHttp(res)
|
||||
const fail = (message: string, cause: unknown, body?: string) =>
|
||||
new AIError({ reason: new InvalidProviderOutputError({ route: "system-one", message, body, http, cause }) })
|
||||
const text = yield* res.text.pipe(
|
||||
|
|
@ -182,10 +184,11 @@ export const model = <Options extends EvaluationOptions = EvaluationOptions>(cfg
|
|||
? new Usage({
|
||||
inputTokens: data.usage.input_tokens,
|
||||
outputTokens: data.usage.output_tokens,
|
||||
totalTokens:
|
||||
data.usage.input_tokens === undefined && data.usage.output_tokens === undefined
|
||||
? undefined
|
||||
: (data.usage.input_tokens ?? 0) + (data.usage.output_tokens ?? 0),
|
||||
totalTokens: ProviderShared.totalTokens(
|
||||
data.usage.input_tokens,
|
||||
data.usage.output_tokens,
|
||||
undefined,
|
||||
),
|
||||
providerMetadata: { [cfg.providerMetadataKey]: data.usage },
|
||||
})
|
||||
: undefined,
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import { Effect, FileSystem, Schema, SchemaGetter } from "effect"
|
|||
import { Base64 } from "effect/encoding"
|
||||
import { HttpClientRequest } from "effect/http"
|
||||
import { ProviderID } from "./schema/ids.js"
|
||||
import { AIError, HttpContext, InvalidProviderOutputError, InvalidRequestError } from "./schema/errors.js"
|
||||
import { AIError, InvalidProviderOutputError, InvalidRequestError } from "./schema/errors.js"
|
||||
import { ProviderMetadata } from "./schema/options.js"
|
||||
import { Service } from "./route/executor-service.js"
|
||||
import { responseHttp, Service } from "./route/executor-service.js"
|
||||
import { detectMediaType, fileMediaType } from "./utils/media-type.js"
|
||||
|
||||
export { detectMediaType } from "./utils/media-type.js"
|
||||
|
|
@ -254,7 +254,7 @@ const download = Effect.fn("Media.download")(function* (
|
|||
new AIError({
|
||||
reason: new InvalidProviderOutputError({
|
||||
message: `Failed to read media from ${source.url}`,
|
||||
http: new HttpContext({ url: response.request.url, status: response.status, headers: response.headers }),
|
||||
http: responseHttp(response),
|
||||
cause,
|
||||
}),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ const Assistant = Schema.StructWithRest(
|
|||
tool_calls: optionalArray(ToolCall),
|
||||
thought_signature: Schema.optional(Schema.String),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
[JsonObject],
|
||||
)
|
||||
const Messages = Schema.Array(
|
||||
Schema.Union([
|
||||
|
|
@ -130,11 +130,11 @@ const Delta = Schema.StructWithRest(
|
|||
tool_calls: optionalNull(Schema.Array(ToolDelta)),
|
||||
thought_signature: optionalNull(Schema.String),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
[JsonObject],
|
||||
)
|
||||
const Choice = Schema.StructWithRest(
|
||||
Schema.Struct({ ...OpenAIChat.OpenAIChatChoice.schema.fields, delta: optionalNull(Delta) }),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
[JsonObject],
|
||||
)
|
||||
const Usage = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
|
|
@ -146,11 +146,11 @@ const Usage = Schema.StructWithRest(
|
|||
cache_write_tokens: optionalNull(Schema.Number),
|
||||
cache_creation_input_tokens: optionalNull(Schema.Number),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
[JsonObject],
|
||||
),
|
||||
),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
[JsonObject],
|
||||
)
|
||||
const Event = Schema.StructWithRest(
|
||||
Schema.Struct({
|
||||
|
|
@ -165,7 +165,7 @@ const Event = Schema.StructWithRest(
|
|||
}),
|
||||
),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
||||
[JsonObject],
|
||||
)
|
||||
|
||||
interface State {
|
||||
|
|
|
|||
|
|
@ -11,18 +11,18 @@ import {
|
|||
import { AnthropicMessages } from "../protocols/anthropic-messages.js"
|
||||
import { OpenAIChat } from "../protocols/openai-chat.js"
|
||||
import { OpenResponses } from "../protocols/open-responses.js"
|
||||
import { optionalNull, ProviderShared } from "../protocols/shared.js"
|
||||
import { JsonObject, optionalNull, ProviderShared } from "../protocols/shared.js"
|
||||
import { gatewayProtocol } from "../protocols/utils/gateway-protocol.js"
|
||||
import type { ProviderPackage } from "../provider-package.js"
|
||||
import { Auth } from "../route/auth.js"
|
||||
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options.js"
|
||||
import { Route, type RouteDefaultsInput } from "../route/client.js"
|
||||
import { Endpoint } from "../route/endpoint.js"
|
||||
import { RequestExecutor } from "../route/executor.js"
|
||||
import { Framing } from "../route/framing.js"
|
||||
import type { Protocol } from "../route/protocol.js"
|
||||
import {
|
||||
AIError,
|
||||
HttpContext,
|
||||
HttpOptions,
|
||||
InvalidProviderOutputError,
|
||||
InvalidRequestError,
|
||||
|
|
@ -30,6 +30,7 @@ import {
|
|||
ModelID,
|
||||
ProviderID,
|
||||
ProviderMetadata,
|
||||
ProviderOptions,
|
||||
ReasoningEffort,
|
||||
Usage,
|
||||
} from "../schema/index.js"
|
||||
|
|
@ -125,7 +126,7 @@ export type Settings = ProviderPackage.Settings & ProviderOptionsInput & { reado
|
|||
|
||||
const GatewayOptionsSchema = Schema.Struct({
|
||||
gateway: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
upstream: Schema.optional(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown))),
|
||||
upstream: Schema.optional(ProviderMetadata),
|
||||
reasoningEffort: Schema.optional(ReasoningEffort),
|
||||
thinking: Schema.optional(
|
||||
Schema.Struct({
|
||||
|
|
@ -233,9 +234,9 @@ const Request = Schema.StructWithRest(
|
|||
model: Schema.String,
|
||||
state: EvaluationInput,
|
||||
questions: Schema.Record(Schema.String, EvaluationQuestion),
|
||||
providerOptions: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
providerOptions: Schema.optional(ProviderOptions),
|
||||
}),
|
||||
[Schema.Record(Schema.String, Schema.Any)],
|
||||
[JsonObject],
|
||||
)
|
||||
const Response = Schema.Struct({
|
||||
model: optionalNull(Schema.String),
|
||||
|
|
@ -310,7 +311,7 @@ export const configure = (input: Options = {}) => {
|
|||
HttpClientRequest.bodyText(body, "application/json"),
|
||||
),
|
||||
)
|
||||
const http = new HttpContext({ url: res.request.url, status: res.status, headers: res.headers })
|
||||
const http = RequestExecutor.responseHttp(res)
|
||||
const fail = (message: string, cause: unknown, body?: string) =>
|
||||
new AIError({
|
||||
reason: new InvalidProviderOutputError({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Context, type Effect } from "effect"
|
||||
import type { HttpClientRequest, HttpClientResponse } from "effect/http"
|
||||
import type { AIError } from "../schema/errors.js"
|
||||
import type { Headers, HttpClientRequest, HttpClientResponse } from "effect/http"
|
||||
import { HttpContext, type AIError } from "../schema/errors.js"
|
||||
|
||||
// The service tag lives in its own leaf module so `Media.Asset` (imported by the schema layer) can require the
|
||||
// executor without pulling the full executor implementation, and therefore the schema barrel, into a cycle.
|
||||
|
|
@ -24,3 +24,13 @@ export class RequestExecutorService extends Context.Service<RequestExecutorServi
|
|||
) {}
|
||||
export const Service = RequestExecutorService
|
||||
export type Service = RequestExecutorService
|
||||
|
||||
export const headerDetails = (headers: Headers.Headers) =>
|
||||
Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, String(value)]))
|
||||
|
||||
export const responseHttp = (response: HttpClientResponse.HttpClientResponse) =>
|
||||
new HttpContext({
|
||||
url: response.request.url,
|
||||
status: response.status,
|
||||
headers: headerDetails(response.headers),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,14 +9,11 @@ import {
|
|||
} from "effect/http"
|
||||
import { HttpContext, HttpRateLimitDetails, AIError, TransportError } from "../schema/index.js"
|
||||
import { classifyProviderFailure, providerErrorMessage } from "../provider-error.js"
|
||||
import { Service, type HttpMiddleware, type Interface } from "./executor-service.js"
|
||||
import { headerDetails, responseHttp, Service, type HttpMiddleware, type Interface } from "./executor-service.js"
|
||||
|
||||
export { Service } from "./executor-service.js"
|
||||
export { responseHttp, Service } from "./executor-service.js"
|
||||
export type { HttpHandler, HttpMiddleware, Interface } from "./executor-service.js"
|
||||
|
||||
const headerDetails = (headers: Headers.Headers) =>
|
||||
Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, String(value)]))
|
||||
|
||||
const normalizedHeaders = (headers: Headers.Headers) =>
|
||||
Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]))
|
||||
|
||||
|
|
@ -79,13 +76,6 @@ const rateLimitDetails = (headers: Record<string, string>, retryAfter: number |
|
|||
})
|
||||
}
|
||||
|
||||
export const responseHttp = (response: HttpClientResponse.HttpClientResponse) =>
|
||||
new HttpContext({
|
||||
url: response.request.url,
|
||||
status: response.status,
|
||||
headers: headerDetails(response.headers),
|
||||
})
|
||||
|
||||
const MAX_BODY_CHARS = 2000
|
||||
|
||||
// Without a recognized message, show the raw body so the provider's explanation is never dropped.
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ import { HttpClientResponse } from "effect/http"
|
|||
import type { Snapshot, Status } from "../generation.js"
|
||||
import { Media } from "../media.js"
|
||||
import type { AuthInput } from "./auth.js"
|
||||
import { RequestExecutor } from "./executor.js"
|
||||
import {
|
||||
AIError,
|
||||
AuthenticationError,
|
||||
ContentPolicyError,
|
||||
HttpContext,
|
||||
type HttpContext,
|
||||
InvalidProviderOutputError,
|
||||
InvalidRequestError,
|
||||
ProviderID,
|
||||
|
|
@ -200,9 +201,6 @@ const FAILURES = {
|
|||
|
||||
export type Failure = keyof typeof FAILURES
|
||||
|
||||
const context = (response: HttpClientResponse.HttpClientResponse) =>
|
||||
new HttpContext({ url: response.request.url, status: response.status, headers: response.headers })
|
||||
|
||||
/** One protocol's route id, display name, and provider, with the decoders and errors that carry them. */
|
||||
export const identity = (input: { readonly id: string; readonly name: string; readonly provider: string }) => {
|
||||
const provider = ProviderID.make(input.provider)
|
||||
|
|
@ -217,7 +215,7 @@ export const identity = (input: { readonly id: string; readonly name: string; re
|
|||
* `contentPolicy` is a moderated result.
|
||||
*/
|
||||
const text = Effect.fn("MediaProtocol.text")(function* (response: HttpClientResponse.HttpClientResponse) {
|
||||
const http = context(response)
|
||||
const http = RequestExecutor.responseHttp(response)
|
||||
const body = yield* response.text.pipe(
|
||||
Effect.mapError(
|
||||
(cause) =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue