mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-18 13:49:25 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e3b26ac47 |
@@ -283,27 +283,21 @@ const AnthropicStreamDelta = Schema.Struct({
|
|||||||
stop_sequence: optionalNull(Schema.String),
|
stop_sequence: optionalNull(Schema.String),
|
||||||
})
|
})
|
||||||
|
|
||||||
const AnthropicEvent = Schema.StructWithRest(
|
const AnthropicEvent = Schema.Struct({
|
||||||
Schema.Struct({
|
type: Schema.String,
|
||||||
type: Schema.String,
|
index: Schema.optional(Schema.Number),
|
||||||
index: Schema.optional(Schema.Number),
|
message: Schema.optional(Schema.Struct({ usage: Schema.optional(AnthropicUsage) })),
|
||||||
message: Schema.optional(Schema.Struct({ usage: Schema.optional(AnthropicUsage) })),
|
content_block: Schema.optional(AnthropicStreamBlock),
|
||||||
content_block: Schema.optional(AnthropicStreamBlock),
|
delta: Schema.optional(AnthropicStreamDelta),
|
||||||
delta: Schema.optional(AnthropicStreamDelta),
|
usage: Schema.optional(AnthropicUsage),
|
||||||
usage: Schema.optional(AnthropicUsage),
|
// `type` and `message` are both required per Anthropic's spec, but
|
||||||
// `type` and `message` are both required per Anthropic's spec, but
|
// OpenAI-compatible proxies and gateway translations occasionally drop one
|
||||||
// OpenAI-compatible proxies and gateway translations occasionally drop one
|
// or the other; mark them optional so a partial payload still parses and
|
||||||
// or the other; mark them optional so a partial payload still parses and
|
// the parser can fall back to whichever field is populated.
|
||||||
// the parser can fall back to whichever field is populated.
|
error: Schema.optional(
|
||||||
error: Schema.optional(
|
Schema.Struct({ type: Schema.optional(Schema.String), message: Schema.optional(Schema.String) }),
|
||||||
Schema.StructWithRest(
|
),
|
||||||
Schema.Struct({ type: Schema.optional(Schema.String), message: Schema.optional(Schema.String) }),
|
})
|
||||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
|
||||||
)
|
|
||||||
type AnthropicEvent = Schema.Schema.Type<typeof AnthropicEvent>
|
type AnthropicEvent = Schema.Schema.Type<typeof AnthropicEvent>
|
||||||
|
|
||||||
interface ParserState {
|
interface ParserState {
|
||||||
@@ -989,11 +983,7 @@ const onError = (event: AnthropicEvent) =>
|
|||||||
new AIError({
|
new AIError({
|
||||||
module: ADAPTER,
|
module: ADAPTER,
|
||||||
method: "stream",
|
method: "stream",
|
||||||
reason: classifyProviderFailure({
|
reason: classifyProviderFailure({ message: providerErrorMessage(event), code: event.error?.type }),
|
||||||
message: providerErrorMessage(event),
|
|
||||||
code: event.error?.type,
|
|
||||||
data: Schema.decodeUnknownSync(Schema.Json)(event),
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const step = (state: ParserState, event: AnthropicEvent) => {
|
const step = (state: ParserState, event: AnthropicEvent) => {
|
||||||
|
|||||||
@@ -155,75 +155,69 @@ const BedrockUsageSchema = Schema.Struct({
|
|||||||
})
|
})
|
||||||
type BedrockUsageSchema = Schema.Schema.Type<typeof BedrockUsageSchema>
|
type BedrockUsageSchema = Schema.Schema.Type<typeof BedrockUsageSchema>
|
||||||
|
|
||||||
const BedrockStreamException = Schema.StructWithRest(
|
const BedrockStreamException = Schema.Struct({
|
||||||
Schema.Struct({
|
message: Schema.optional(Schema.String),
|
||||||
message: Schema.optional(Schema.String),
|
originalMessage: Schema.optional(Schema.String),
|
||||||
originalMessage: Schema.optional(Schema.String),
|
originalStatusCode: Schema.optional(Schema.Number),
|
||||||
originalStatusCode: Schema.optional(Schema.Number),
|
})
|
||||||
}),
|
|
||||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Streaming event shape — the AWS event stream wraps each JSON payload by its
|
// Streaming event shape — the AWS event stream wraps each JSON payload by its
|
||||||
// `:event-type` header (e.g. `messageStart`, `contentBlockDelta`). We
|
// `:event-type` header (e.g. `messageStart`, `contentBlockDelta`). We
|
||||||
// reconstruct that wrapping in `decodeFrames` below so the event schema can
|
// reconstruct that wrapping in `decodeFrames` below so the event schema can
|
||||||
// stay a plain discriminated record.
|
// stay a plain discriminated record.
|
||||||
const BedrockEvent = Schema.StructWithRest(
|
const BedrockEvent = Schema.Struct({
|
||||||
Schema.Struct({
|
messageStart: Schema.optional(Schema.Struct({ role: Schema.String })),
|
||||||
messageStart: Schema.optional(Schema.Struct({ role: Schema.String })),
|
contentBlockStart: Schema.optional(
|
||||||
contentBlockStart: Schema.optional(
|
Schema.Struct({
|
||||||
Schema.Struct({
|
contentBlockIndex: Schema.Number,
|
||||||
contentBlockIndex: Schema.Number,
|
start: Schema.optional(
|
||||||
start: Schema.optional(
|
Schema.Struct({
|
||||||
Schema.Struct({
|
toolUse: Schema.optional(Schema.Struct({ toolUseId: Schema.String, name: Schema.String })),
|
||||||
toolUse: Schema.optional(Schema.Struct({ toolUseId: Schema.String, name: Schema.String })),
|
}),
|
||||||
}),
|
),
|
||||||
),
|
}),
|
||||||
}),
|
),
|
||||||
),
|
contentBlockDelta: Schema.optional(
|
||||||
contentBlockDelta: Schema.optional(
|
Schema.Struct({
|
||||||
Schema.Struct({
|
contentBlockIndex: Schema.Number,
|
||||||
contentBlockIndex: Schema.Number,
|
delta: Schema.optional(
|
||||||
delta: Schema.optional(
|
Schema.Struct({
|
||||||
Schema.Struct({
|
text: Schema.optional(Schema.String),
|
||||||
text: Schema.optional(Schema.String),
|
toolUse: Schema.optional(Schema.Struct({ input: Schema.String })),
|
||||||
toolUse: Schema.optional(Schema.Struct({ input: Schema.String })),
|
reasoningContent: Schema.optional(
|
||||||
reasoningContent: Schema.optional(
|
Schema.Struct({
|
||||||
Schema.Struct({
|
text: Schema.optional(Schema.String),
|
||||||
text: Schema.optional(Schema.String),
|
signature: Schema.optional(Schema.String),
|
||||||
signature: Schema.optional(Schema.String),
|
// Blob fields in Bedrock's JSON event stream are base64 strings.
|
||||||
// Blob fields in Bedrock's JSON event stream are base64 strings.
|
redactedContent: Schema.optional(Schema.String),
|
||||||
redactedContent: Schema.optional(Schema.String),
|
// Vercel's Bedrock provider exposes the same delta under
|
||||||
// Vercel's Bedrock provider exposes the same delta under
|
// Anthropic's shorter `data` spelling.
|
||||||
// Anthropic's shorter `data` spelling.
|
data: Schema.optional(Schema.String),
|
||||||
data: Schema.optional(Schema.String),
|
}),
|
||||||
}),
|
),
|
||||||
),
|
}),
|
||||||
}),
|
),
|
||||||
),
|
}),
|
||||||
}),
|
),
|
||||||
),
|
contentBlockStop: Schema.optional(Schema.Struct({ contentBlockIndex: Schema.Number })),
|
||||||
contentBlockStop: Schema.optional(Schema.Struct({ contentBlockIndex: Schema.Number })),
|
messageStop: Schema.optional(
|
||||||
messageStop: Schema.optional(
|
Schema.Struct({
|
||||||
Schema.Struct({
|
stopReason: Schema.String,
|
||||||
stopReason: Schema.String,
|
additionalModelResponseFields: Schema.optional(Schema.Unknown),
|
||||||
additionalModelResponseFields: Schema.optional(Schema.Unknown),
|
}),
|
||||||
}),
|
),
|
||||||
),
|
metadata: Schema.optional(
|
||||||
metadata: Schema.optional(
|
Schema.Struct({
|
||||||
Schema.Struct({
|
usage: Schema.optional(BedrockUsageSchema),
|
||||||
usage: Schema.optional(BedrockUsageSchema),
|
metrics: Schema.optional(Schema.Unknown),
|
||||||
metrics: Schema.optional(Schema.Unknown),
|
}),
|
||||||
}),
|
),
|
||||||
),
|
internalServerException: Schema.optional(BedrockStreamException),
|
||||||
internalServerException: Schema.optional(BedrockStreamException),
|
modelStreamErrorException: Schema.optional(BedrockStreamException),
|
||||||
modelStreamErrorException: Schema.optional(BedrockStreamException),
|
validationException: Schema.optional(BedrockStreamException),
|
||||||
validationException: Schema.optional(BedrockStreamException),
|
throttlingException: Schema.optional(BedrockStreamException),
|
||||||
throttlingException: Schema.optional(BedrockStreamException),
|
serviceUnavailableException: Schema.optional(BedrockStreamException),
|
||||||
serviceUnavailableException: Schema.optional(BedrockStreamException),
|
})
|
||||||
}),
|
|
||||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
|
||||||
)
|
|
||||||
type BedrockEvent = Schema.Schema.Type<typeof BedrockEvent>
|
type BedrockEvent = Schema.Schema.Type<typeof BedrockEvent>
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -672,7 +666,6 @@ const step = (state: ParserState, event: BedrockEvent) =>
|
|||||||
reason: classifyProviderFailure({
|
reason: classifyProviderFailure({
|
||||||
message: exception[1]?.message ?? exception[1]?.originalMessage ?? "Bedrock Converse stream error",
|
message: exception[1]?.message ?? exception[1]?.originalMessage ?? "Bedrock Converse stream error",
|
||||||
code: exception[0],
|
code: exception[0],
|
||||||
data: Schema.decodeUnknownSync(Schema.Json)(event),
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1012,12 +1012,7 @@ export const providerFailure = (id: string, event: Event, fallback: string) => {
|
|||||||
return new AIError({
|
return new AIError({
|
||||||
module: id,
|
module: id,
|
||||||
method: "stream",
|
method: "stream",
|
||||||
reason: classifyProviderFailure({
|
reason: classifyProviderFailure({ message, code, status }),
|
||||||
message,
|
|
||||||
code,
|
|
||||||
status,
|
|
||||||
data: Schema.decodeUnknownSync(Schema.Json)(event),
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -209,22 +209,16 @@ const OpenAIChatChoice = Schema.Struct({
|
|||||||
native_finish_reason: optionalNull(Schema.String),
|
native_finish_reason: optionalNull(Schema.String),
|
||||||
})
|
})
|
||||||
|
|
||||||
const OpenAIChatError = Schema.StructWithRest(
|
const OpenAIChatError = Schema.Struct({
|
||||||
Schema.Struct({
|
code: optionalNull(Schema.Union([Schema.String, Schema.Number])),
|
||||||
code: optionalNull(Schema.Union([Schema.String, Schema.Number])),
|
message: Schema.String,
|
||||||
message: Schema.String,
|
})
|
||||||
}),
|
|
||||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
|
||||||
)
|
|
||||||
|
|
||||||
export const OpenAIChatEvent = Schema.StructWithRest(
|
export const OpenAIChatEvent = Schema.Struct({
|
||||||
Schema.Struct({
|
choices: optionalNull(Schema.Array(OpenAIChatChoice)),
|
||||||
choices: optionalNull(Schema.Array(OpenAIChatChoice)),
|
usage: optionalNull(OpenAIChatUsage),
|
||||||
usage: optionalNull(OpenAIChatUsage),
|
error: optionalNull(OpenAIChatError),
|
||||||
error: optionalNull(OpenAIChatError),
|
})
|
||||||
}),
|
|
||||||
[Schema.Record(Schema.String, Schema.Unknown)],
|
|
||||||
)
|
|
||||||
export type OpenAIChatEvent = Schema.Schema.Type<typeof OpenAIChatEvent>
|
export type OpenAIChatEvent = Schema.Schema.Type<typeof OpenAIChatEvent>
|
||||||
type OpenAIChatRequestMessage = LLMRequest["messages"][number]
|
type OpenAIChatRequestMessage = LLMRequest["messages"][number]
|
||||||
|
|
||||||
@@ -693,7 +687,6 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
|
|||||||
message: event.error.message,
|
message: event.error.message,
|
||||||
code: event.error.code === undefined || event.error.code === null ? undefined : String(event.error.code),
|
code: event.error.code === undefined || event.error.code === null ? undefined : String(event.error.code),
|
||||||
status: typeof event.error.code === "number" ? event.error.code : undefined,
|
status: typeof event.error.code === "number" ? event.error.code : undefined,
|
||||||
data: Schema.decodeUnknownSync(Schema.Json)(event),
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
const events: LLMEvent[] = []
|
const events: LLMEvent[] = []
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ const CONTENT_POLICY_TEXT = /content[-_\s]?policy|content_filter|safety/i
|
|||||||
|
|
||||||
export interface ProviderFailure {
|
export interface ProviderFailure {
|
||||||
readonly message: string
|
readonly message: string
|
||||||
readonly data: typeof Schema.Json.Type
|
|
||||||
readonly status?: number | undefined
|
readonly status?: number | undefined
|
||||||
readonly code?: string | undefined
|
readonly code?: string | undefined
|
||||||
readonly retryAfterMs?: number | undefined
|
readonly retryAfterMs?: number | undefined
|
||||||
@@ -94,12 +93,7 @@ export function classifyProviderFailure(input: ProviderFailure): AIError["reason
|
|||||||
.filter((code): code is string => code !== undefined)
|
.filter((code): code is string => code !== undefined)
|
||||||
.map((code) => code.toLowerCase())
|
.map((code) => code.toLowerCase())
|
||||||
const text = body || input.message
|
const text = body || input.message
|
||||||
const common = {
|
const common = { message: input.message, providerMetadata: input.providerMetadata, http: input.http }
|
||||||
message: input.message,
|
|
||||||
data: input.data,
|
|
||||||
providerMetadata: input.providerMetadata,
|
|
||||||
http: input.http,
|
|
||||||
}
|
|
||||||
const clientScoped = input.status === undefined || (input.status >= 400 && input.status < 500)
|
const clientScoped = input.status === undefined || (input.status >= 400 && input.status < 500)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -173,7 +173,6 @@ const statusError =
|
|||||||
reason: classifyProviderFailure({
|
reason: classifyProviderFailure({
|
||||||
status: response.status,
|
status: response.status,
|
||||||
message: providerMessage(response.status, body),
|
message: providerMessage(response.status, body),
|
||||||
data: body ?? null,
|
|
||||||
retryAfterMs: retryAfter,
|
retryAfterMs: retryAfter,
|
||||||
rateLimit,
|
rateLimit,
|
||||||
http: responseHttp({
|
http: responseHttp({
|
||||||
@@ -194,7 +193,6 @@ const statusError =
|
|||||||
// request headers are empty.
|
// request headers are empty.
|
||||||
export const classifyHttpFailure = (input: {
|
export const classifyHttpFailure = (input: {
|
||||||
readonly message: string
|
readonly message: string
|
||||||
readonly data: typeof Schema.Json.Type
|
|
||||||
readonly url: string
|
readonly url: string
|
||||||
readonly status?: number | undefined
|
readonly status?: number | undefined
|
||||||
readonly code?: string | undefined
|
readonly code?: string | undefined
|
||||||
@@ -207,7 +205,6 @@ export const classifyHttpFailure = (input: {
|
|||||||
const details = responseBody(input.responseBody)
|
const details = responseBody(input.responseBody)
|
||||||
return classifyProviderFailure({
|
return classifyProviderFailure({
|
||||||
message: input.message,
|
message: input.message,
|
||||||
data: input.data,
|
|
||||||
status: input.status,
|
status: input.status,
|
||||||
code: input.code,
|
code: input.code,
|
||||||
retryAfterMs: retryAfter,
|
retryAfterMs: retryAfter,
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export class HttpContext extends Schema.Class<HttpContext>("AI.HttpContext")({
|
|||||||
export class InvalidRequestReason extends Schema.Class<InvalidRequestReason>("AI.Error.InvalidRequest")({
|
export class InvalidRequestReason extends Schema.Class<InvalidRequestReason>("AI.Error.InvalidRequest")({
|
||||||
_tag: Schema.tag("InvalidRequest"),
|
_tag: Schema.tag("InvalidRequest"),
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
data: Schema.optional(Schema.Json),
|
|
||||||
parameter: Schema.optional(Schema.String),
|
parameter: Schema.optional(Schema.String),
|
||||||
classification: Schema.optional(ProviderFailureClassification),
|
classification: Schema.optional(ProviderFailureClassification),
|
||||||
providerMetadata: Schema.optional(ProviderMetadata),
|
providerMetadata: Schema.optional(ProviderMetadata),
|
||||||
@@ -56,7 +55,6 @@ export class NoRouteReason extends Schema.Class<NoRouteReason>("AI.Error.NoRoute
|
|||||||
export class AuthenticationReason extends Schema.Class<AuthenticationReason>("AI.Error.Authentication")({
|
export class AuthenticationReason extends Schema.Class<AuthenticationReason>("AI.Error.Authentication")({
|
||||||
_tag: Schema.tag("Authentication"),
|
_tag: Schema.tag("Authentication"),
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
data: Schema.optional(Schema.Json),
|
|
||||||
kind: Schema.Literals(["missing", "invalid", "expired", "insufficient-permissions", "unknown"]),
|
kind: Schema.Literals(["missing", "invalid", "expired", "insufficient-permissions", "unknown"]),
|
||||||
providerMetadata: Schema.optional(ProviderMetadata),
|
providerMetadata: Schema.optional(ProviderMetadata),
|
||||||
http: Schema.optional(HttpContext),
|
http: Schema.optional(HttpContext),
|
||||||
@@ -65,7 +63,6 @@ export class AuthenticationReason extends Schema.Class<AuthenticationReason>("AI
|
|||||||
export class RateLimitReason extends Schema.Class<RateLimitReason>("AI.Error.RateLimit")({
|
export class RateLimitReason extends Schema.Class<RateLimitReason>("AI.Error.RateLimit")({
|
||||||
_tag: Schema.tag("RateLimit"),
|
_tag: Schema.tag("RateLimit"),
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
data: Schema.optional(Schema.Json),
|
|
||||||
retryAfterMs: Schema.optional(Schema.Number),
|
retryAfterMs: Schema.optional(Schema.Number),
|
||||||
rateLimit: Schema.optional(HttpRateLimitDetails),
|
rateLimit: Schema.optional(HttpRateLimitDetails),
|
||||||
providerMetadata: Schema.optional(ProviderMetadata),
|
providerMetadata: Schema.optional(ProviderMetadata),
|
||||||
@@ -75,7 +72,6 @@ export class RateLimitReason extends Schema.Class<RateLimitReason>("AI.Error.Rat
|
|||||||
export class QuotaExceededReason extends Schema.Class<QuotaExceededReason>("AI.Error.QuotaExceeded")({
|
export class QuotaExceededReason extends Schema.Class<QuotaExceededReason>("AI.Error.QuotaExceeded")({
|
||||||
_tag: Schema.tag("QuotaExceeded"),
|
_tag: Schema.tag("QuotaExceeded"),
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
data: Schema.optional(Schema.Json),
|
|
||||||
providerMetadata: Schema.optional(ProviderMetadata),
|
providerMetadata: Schema.optional(ProviderMetadata),
|
||||||
http: Schema.optional(HttpContext),
|
http: Schema.optional(HttpContext),
|
||||||
}) {}
|
}) {}
|
||||||
@@ -83,7 +79,6 @@ export class QuotaExceededReason extends Schema.Class<QuotaExceededReason>("AI.E
|
|||||||
export class ContentPolicyReason extends Schema.Class<ContentPolicyReason>("AI.Error.ContentPolicy")({
|
export class ContentPolicyReason extends Schema.Class<ContentPolicyReason>("AI.Error.ContentPolicy")({
|
||||||
_tag: Schema.tag("ContentPolicy"),
|
_tag: Schema.tag("ContentPolicy"),
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
data: Schema.optional(Schema.Json),
|
|
||||||
providerMetadata: Schema.optional(ProviderMetadata),
|
providerMetadata: Schema.optional(ProviderMetadata),
|
||||||
http: Schema.optional(HttpContext),
|
http: Schema.optional(HttpContext),
|
||||||
}) {}
|
}) {}
|
||||||
@@ -91,7 +86,6 @@ export class ContentPolicyReason extends Schema.Class<ContentPolicyReason>("AI.E
|
|||||||
export class ProviderInternalReason extends Schema.Class<ProviderInternalReason>("AI.Error.ProviderInternal")({
|
export class ProviderInternalReason extends Schema.Class<ProviderInternalReason>("AI.Error.ProviderInternal")({
|
||||||
_tag: Schema.tag("ProviderInternal"),
|
_tag: Schema.tag("ProviderInternal"),
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
data: Schema.optional(Schema.Json),
|
|
||||||
status: Schema.optional(Schema.Number),
|
status: Schema.optional(Schema.Number),
|
||||||
retryAfterMs: Schema.optional(Schema.Number),
|
retryAfterMs: Schema.optional(Schema.Number),
|
||||||
providerMetadata: Schema.optional(ProviderMetadata),
|
providerMetadata: Schema.optional(ProviderMetadata),
|
||||||
@@ -135,7 +129,6 @@ export class InvalidProviderOutputReason extends Schema.Class<InvalidProviderOut
|
|||||||
export class UnknownProviderReason extends Schema.Class<UnknownProviderReason>("AI.Error.UnknownProvider")({
|
export class UnknownProviderReason extends Schema.Class<UnknownProviderReason>("AI.Error.UnknownProvider")({
|
||||||
_tag: Schema.tag("UnknownProvider"),
|
_tag: Schema.tag("UnknownProvider"),
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
data: Schema.optional(Schema.Json),
|
|
||||||
status: Schema.optional(Schema.Number),
|
status: Schema.optional(Schema.Number),
|
||||||
providerMetadata: Schema.optional(ProviderMetadata),
|
providerMetadata: Schema.optional(ProviderMetadata),
|
||||||
http: Schema.optional(HttpContext),
|
http: Schema.optional(HttpContext),
|
||||||
|
|||||||
@@ -216,7 +216,6 @@ export type Finish = Schema.Schema.Type<typeof Finish>
|
|||||||
export const ProviderErrorEvent = Schema.Struct({
|
export const ProviderErrorEvent = Schema.Struct({
|
||||||
type: Schema.tag("provider-error"),
|
type: Schema.tag("provider-error"),
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
data: Schema.Json,
|
|
||||||
classification: Schema.optional(ProviderFailureClassification),
|
classification: Schema.optional(ProviderFailureClassification),
|
||||||
providerMetadata: Schema.optional(ProviderMetadata),
|
providerMetadata: Schema.optional(ProviderMetadata),
|
||||||
}).annotate({ identifier: "LLM.Event.ProviderError" })
|
}).annotate({ identifier: "LLM.Event.ProviderError" })
|
||||||
|
|||||||
@@ -1031,24 +1031,12 @@ describe("Anthropic Messages route", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const error = yield* LLMClient.generate(request).pipe(
|
const error = yield* LLMClient.generate(request).pipe(
|
||||||
Effect.provide(
|
Effect.provide(
|
||||||
fixedResponse(
|
fixedResponse(sseEvents({ type: "error", error: { type: "overloaded_error", message: "Overloaded" } })),
|
||||||
sseEvents({
|
|
||||||
type: "error",
|
|
||||||
error: { type: "overloaded_error", message: "Overloaded", request_id: "req_123" },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Effect.flip,
|
Effect.flip,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(error.reason).toMatchObject({
|
expect(error.reason).toMatchObject({ _tag: "ProviderInternal", message: "overloaded_error: Overloaded" })
|
||||||
_tag: "ProviderInternal",
|
|
||||||
message: "overloaded_error: Overloaded",
|
|
||||||
data: {
|
|
||||||
type: "error",
|
|
||||||
error: { type: "overloaded_error", message: "Overloaded", request_id: "req_123" },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -714,15 +714,11 @@ describe("Bedrock Converse route", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const body = concat([
|
const body = concat([
|
||||||
eventFrame("messageStart", { role: "assistant" }),
|
eventFrame("messageStart", { role: "assistant" }),
|
||||||
exceptionFrame("throttlingException", { message: "Slow down", requestId: "req_123" }),
|
exceptionFrame("throttlingException", { message: "Slow down" }),
|
||||||
])
|
])
|
||||||
const error = yield* LLMClient.generate(baseRequest).pipe(Effect.provide(fixedBytes(body)), Effect.flip)
|
const error = yield* LLMClient.generate(baseRequest).pipe(Effect.provide(fixedBytes(body)), Effect.flip)
|
||||||
|
|
||||||
expect(error.reason).toMatchObject({
|
expect(error.reason).toMatchObject({ _tag: "RateLimit", message: "Slow down" })
|
||||||
_tag: "RateLimit",
|
|
||||||
message: "Slow down",
|
|
||||||
data: { throttlingException: { message: "Slow down", requestId: "req_123" } },
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2592,25 +2592,13 @@ describe("OpenAI Responses route", () => {
|
|||||||
message: "Something went wrong",
|
message: "Something went wrong",
|
||||||
param: null,
|
param: null,
|
||||||
sequence_number: 1,
|
sequence_number: 1,
|
||||||
diagnostic: { region: "us-east" },
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Effect.flip,
|
Effect.flip,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(error.reason).toMatchObject({
|
expect(error.reason).toMatchObject({ _tag: "UnknownProvider", message: "Something went wrong" })
|
||||||
_tag: "UnknownProvider",
|
|
||||||
message: "Something went wrong",
|
|
||||||
data: {
|
|
||||||
type: "error",
|
|
||||||
code: null,
|
|
||||||
message: "Something went wrong",
|
|
||||||
param: null,
|
|
||||||
sequence_number: 1,
|
|
||||||
diagnostic: { region: "us-east" },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -253,17 +253,14 @@ describe("OpenRouter", () => {
|
|||||||
Effect.provide(
|
Effect.provide(
|
||||||
fixedResponse(
|
fixedResponse(
|
||||||
sseEvents({
|
sseEvents({
|
||||||
error: { code: 502, message: "Provider disconnected", upstream: "openai" },
|
error: { code: 502, message: "Provider disconnected" },
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Effect.flip,
|
Effect.flip,
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(error.reason).toMatchObject({
|
expect(error.reason).toMatchObject({ _tag: "ProviderInternal" })
|
||||||
_tag: "ProviderInternal",
|
|
||||||
data: { error: { code: 502, message: "Provider disconnected", upstream: "openai" } },
|
|
||||||
})
|
|
||||||
expect(error.message).toContain("Provider disconnected")
|
expect(error.message).toContain("Provider disconnected")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -478,12 +478,7 @@ export type Endpoint5_31Output =
|
|||||||
readonly location?: Location.Ref | undefined
|
readonly location?: Location.Ref | undefined
|
||||||
readonly data: {
|
readonly data: {
|
||||||
readonly sessionID: Session.ID
|
readonly sessionID: Session.ID
|
||||||
readonly error: {
|
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
|
||||||
readonly type: string
|
|
||||||
readonly message: string
|
|
||||||
readonly status?: number | undefined
|
|
||||||
readonly data?: Schema.Json | undefined
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -610,12 +605,7 @@ export type Endpoint5_31Output =
|
|||||||
readonly data: {
|
readonly data: {
|
||||||
readonly sessionID: Session.ID
|
readonly sessionID: Session.ID
|
||||||
readonly assistantMessageID: SessionMessage.ID
|
readonly assistantMessageID: SessionMessage.ID
|
||||||
readonly error: {
|
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
|
||||||
readonly type: string
|
|
||||||
readonly message: string
|
|
||||||
readonly status?: number | undefined
|
|
||||||
readonly data?: Schema.Json | undefined
|
|
||||||
}
|
|
||||||
readonly cost?: (number & Brand.Brand<"Money.USD">) | undefined
|
readonly cost?: (number & Brand.Brand<"Money.USD">) | undefined
|
||||||
readonly tokens?:
|
readonly tokens?:
|
||||||
| {
|
| {
|
||||||
@@ -777,12 +767,7 @@ export type Endpoint5_31Output =
|
|||||||
readonly sessionID: Session.ID
|
readonly sessionID: Session.ID
|
||||||
readonly assistantMessageID: SessionMessage.ID
|
readonly assistantMessageID: SessionMessage.ID
|
||||||
readonly id: string
|
readonly id: string
|
||||||
readonly error: {
|
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
|
||||||
readonly type: string
|
|
||||||
readonly message: string
|
|
||||||
readonly status?: number | undefined
|
|
||||||
readonly data?: Schema.Json | undefined
|
|
||||||
}
|
|
||||||
readonly content?:
|
readonly content?:
|
||||||
| readonly [
|
| readonly [
|
||||||
(
|
(
|
||||||
@@ -822,12 +807,7 @@ export type Endpoint5_31Output =
|
|||||||
readonly assistantMessageID: SessionMessage.ID
|
readonly assistantMessageID: SessionMessage.ID
|
||||||
readonly attempt: number
|
readonly attempt: number
|
||||||
readonly at: number
|
readonly at: number
|
||||||
readonly error: {
|
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
|
||||||
readonly type: string
|
|
||||||
readonly message: string
|
|
||||||
readonly status?: number | undefined
|
|
||||||
readonly data?: Schema.Json | undefined
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
@@ -868,12 +848,7 @@ export type Endpoint5_31Output =
|
|||||||
readonly data: {
|
readonly data: {
|
||||||
readonly sessionID: Session.ID
|
readonly sessionID: Session.ID
|
||||||
readonly reason: "auto" | "manual"
|
readonly reason: "auto" | "manual"
|
||||||
readonly error: {
|
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
|
||||||
readonly type: string
|
|
||||||
readonly message: string
|
|
||||||
readonly status?: number | undefined
|
|
||||||
readonly data?: Schema.Json | undefined
|
|
||||||
}
|
|
||||||
readonly inputID?: SessionMessage.ID | undefined
|
readonly inputID?: SessionMessage.ID | undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export type ToolTextContent = { type: "text"; text: string }
|
|||||||
|
|
||||||
export type ToolFileContent = { type: "file"; uri: string; mime: string; name?: string | null }
|
export type ToolFileContent = { type: "file"; uri: string; mime: string; name?: string | null }
|
||||||
|
|
||||||
export type SessionStructuredError = { type: string; message: string; status?: number; data?: JsonValue }
|
export type SessionStructuredError = { type: string; message: string; status?: number }
|
||||||
|
|
||||||
export type SessionMessageCompactionRunning = {
|
export type SessionMessageCompactionRunning = {
|
||||||
type: "compaction"
|
type: "compaction"
|
||||||
@@ -2647,12 +2647,7 @@ export type SessionImportInput = {
|
|||||||
| {
|
| {
|
||||||
readonly status: "error"
|
readonly status: "error"
|
||||||
readonly input: { readonly [x: string]: JsonValue }
|
readonly input: { readonly [x: string]: JsonValue }
|
||||||
readonly error: {
|
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
||||||
readonly type: string
|
|
||||||
readonly message: string
|
|
||||||
readonly status?: number
|
|
||||||
readonly data?: JsonValue
|
|
||||||
}
|
|
||||||
readonly content?: readonly [
|
readonly content?: readonly [
|
||||||
(
|
(
|
||||||
| { readonly type: "text"; readonly text: string }
|
| { readonly type: "text"; readonly text: string }
|
||||||
@@ -2687,21 +2682,11 @@ export type SessionImportInput = {
|
|||||||
readonly reasoning: number
|
readonly reasoning: number
|
||||||
readonly cache: { readonly read: number; readonly write: number }
|
readonly cache: { readonly read: number; readonly write: number }
|
||||||
}
|
}
|
||||||
readonly error?: {
|
readonly error?: { readonly type: string; readonly message: string; readonly status?: number }
|
||||||
readonly type: string
|
|
||||||
readonly message: string
|
|
||||||
readonly status?: number
|
|
||||||
readonly data?: JsonValue
|
|
||||||
}
|
|
||||||
readonly retry?: {
|
readonly retry?: {
|
||||||
readonly attempt: number
|
readonly attempt: number
|
||||||
readonly at: number
|
readonly at: number
|
||||||
readonly error: {
|
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
||||||
readonly type: string
|
|
||||||
readonly message: string
|
|
||||||
readonly status?: number
|
|
||||||
readonly data?: JsonValue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| (
|
| (
|
||||||
@@ -2732,12 +2717,7 @@ export type SessionImportInput = {
|
|||||||
readonly time: { readonly created: number }
|
readonly time: { readonly created: number }
|
||||||
readonly status: "failed"
|
readonly status: "failed"
|
||||||
readonly reason: "auto" | "manual"
|
readonly reason: "auto" | "manual"
|
||||||
readonly error: {
|
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
||||||
readonly type: string
|
|
||||||
readonly message: string
|
|
||||||
readonly status?: number
|
|
||||||
readonly data?: JsonValue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
>
|
>
|
||||||
@@ -2934,12 +2914,7 @@ export type SessionImportInput = {
|
|||||||
| {
|
| {
|
||||||
readonly status: "error"
|
readonly status: "error"
|
||||||
readonly input: { readonly [x: string]: JsonValue }
|
readonly input: { readonly [x: string]: JsonValue }
|
||||||
readonly error: {
|
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
||||||
readonly type: string
|
|
||||||
readonly message: string
|
|
||||||
readonly status?: number
|
|
||||||
readonly data?: JsonValue
|
|
||||||
}
|
|
||||||
readonly content?: readonly [
|
readonly content?: readonly [
|
||||||
(
|
(
|
||||||
| { readonly type: "text"; readonly text: string }
|
| { readonly type: "text"; readonly text: string }
|
||||||
@@ -2974,21 +2949,11 @@ export type SessionImportInput = {
|
|||||||
readonly reasoning: number
|
readonly reasoning: number
|
||||||
readonly cache: { readonly read: number; readonly write: number }
|
readonly cache: { readonly read: number; readonly write: number }
|
||||||
}
|
}
|
||||||
readonly error?: {
|
readonly error?: { readonly type: string; readonly message: string; readonly status?: number }
|
||||||
readonly type: string
|
|
||||||
readonly message: string
|
|
||||||
readonly status?: number
|
|
||||||
readonly data?: JsonValue
|
|
||||||
}
|
|
||||||
readonly retry?: {
|
readonly retry?: {
|
||||||
readonly attempt: number
|
readonly attempt: number
|
||||||
readonly at: number
|
readonly at: number
|
||||||
readonly error: {
|
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
||||||
readonly type: string
|
|
||||||
readonly message: string
|
|
||||||
readonly status?: number
|
|
||||||
readonly data?: JsonValue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| (
|
| (
|
||||||
@@ -3019,12 +2984,7 @@ export type SessionImportInput = {
|
|||||||
readonly time: { readonly created: number }
|
readonly time: { readonly created: number }
|
||||||
readonly status: "failed"
|
readonly status: "failed"
|
||||||
readonly reason: "auto" | "manual"
|
readonly reason: "auto" | "manual"
|
||||||
readonly error: {
|
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
||||||
readonly type: string
|
|
||||||
readonly message: string
|
|
||||||
readonly status?: number
|
|
||||||
readonly data?: JsonValue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
>
|
>
|
||||||
@@ -3221,12 +3181,7 @@ export type SessionImportInput = {
|
|||||||
| {
|
| {
|
||||||
readonly status: "error"
|
readonly status: "error"
|
||||||
readonly input: { readonly [x: string]: JsonValue }
|
readonly input: { readonly [x: string]: JsonValue }
|
||||||
readonly error: {
|
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
||||||
readonly type: string
|
|
||||||
readonly message: string
|
|
||||||
readonly status?: number
|
|
||||||
readonly data?: JsonValue
|
|
||||||
}
|
|
||||||
readonly content?: readonly [
|
readonly content?: readonly [
|
||||||
(
|
(
|
||||||
| { readonly type: "text"; readonly text: string }
|
| { readonly type: "text"; readonly text: string }
|
||||||
@@ -3261,21 +3216,11 @@ export type SessionImportInput = {
|
|||||||
readonly reasoning: number
|
readonly reasoning: number
|
||||||
readonly cache: { readonly read: number; readonly write: number }
|
readonly cache: { readonly read: number; readonly write: number }
|
||||||
}
|
}
|
||||||
readonly error?: {
|
readonly error?: { readonly type: string; readonly message: string; readonly status?: number }
|
||||||
readonly type: string
|
|
||||||
readonly message: string
|
|
||||||
readonly status?: number
|
|
||||||
readonly data?: JsonValue
|
|
||||||
}
|
|
||||||
readonly retry?: {
|
readonly retry?: {
|
||||||
readonly attempt: number
|
readonly attempt: number
|
||||||
readonly at: number
|
readonly at: number
|
||||||
readonly error: {
|
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
||||||
readonly type: string
|
|
||||||
readonly message: string
|
|
||||||
readonly status?: number
|
|
||||||
readonly data?: JsonValue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| (
|
| (
|
||||||
@@ -3306,12 +3251,7 @@ export type SessionImportInput = {
|
|||||||
readonly time: { readonly created: number }
|
readonly time: { readonly created: number }
|
||||||
readonly status: "failed"
|
readonly status: "failed"
|
||||||
readonly reason: "auto" | "manual"
|
readonly reason: "auto" | "manual"
|
||||||
readonly error: {
|
readonly error: { readonly type: string; readonly message: string; readonly status?: number }
|
||||||
readonly type: string
|
|
||||||
readonly message: string
|
|
||||||
readonly status?: number
|
|
||||||
readonly data?: JsonValue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -780,10 +780,7 @@ function llmError(method: string, error: unknown) {
|
|||||||
? new InvalidProviderOutputReason({ message: error.message })
|
? new InvalidProviderOutputReason({ message: error.message })
|
||||||
: APICallError.isInstance(error)
|
: APICallError.isInstance(error)
|
||||||
? apiCallErrorReason(error)
|
? apiCallErrorReason(error)
|
||||||
: new UnknownProviderReason({
|
: new UnknownProviderReason({ message: unknownErrorMessage(error) })
|
||||||
message: unknownErrorMessage(error),
|
|
||||||
data: Schema.decodeUnknownSync(Schema.Json)(jsonValue(error)),
|
|
||||||
})
|
|
||||||
return new AIError({
|
return new AIError({
|
||||||
module: "AISDK",
|
module: "AISDK",
|
||||||
method,
|
method,
|
||||||
@@ -795,7 +792,6 @@ function apiCallErrorReason(error: APICallError) {
|
|||||||
const details = providerErrorDetails(error)
|
const details = providerErrorDetails(error)
|
||||||
const reason = RequestExecutor.classifyHttpFailure({
|
const reason = RequestExecutor.classifyHttpFailure({
|
||||||
message: details.message,
|
message: details.message,
|
||||||
data: Schema.decodeUnknownSync(Schema.Json)(jsonValue(error.data ?? error.responseBody ?? null)),
|
|
||||||
url: error.url,
|
url: error.url,
|
||||||
status: error.statusCode,
|
status: error.statusCode,
|
||||||
code: details.code,
|
code: details.code,
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
export * as ConfigImagePlugin from "./image.js"
|
||||||
|
|
||||||
|
import { define } from "@opencode-ai/plugin/effect/plugin"
|
||||||
|
import { Effect, Stream } from "effect"
|
||||||
|
import { Config } from "../../config.js"
|
||||||
|
import { Image } from "../../image.js"
|
||||||
|
|
||||||
|
export const Plugin = define({
|
||||||
|
id: "opencode.config.image",
|
||||||
|
effect: Effect.fn(function* (ctx) {
|
||||||
|
const config = yield* Config.Service
|
||||||
|
const image = yield* Image.Service
|
||||||
|
const loaded = { entries: yield* config.entries() }
|
||||||
|
yield* image.transform((draft) => {
|
||||||
|
for (const entry of loaded.entries) {
|
||||||
|
if (entry.type !== "document") continue
|
||||||
|
const configured = entry.info.media?.image
|
||||||
|
if (!configured) continue
|
||||||
|
draft.configure({
|
||||||
|
...(configured.auto_resize === undefined ? {} : { autoResize: configured.auto_resize }),
|
||||||
|
...(configured.max_width === undefined ? {} : { maxWidth: configured.max_width }),
|
||||||
|
...(configured.max_height === undefined ? {} : { maxHeight: configured.max_height }),
|
||||||
|
...(configured.max_base64_bytes === undefined ? {} : { maxBase64Bytes: configured.max_base64_bytes }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
yield* ctx.event.subscribe().pipe(
|
||||||
|
Stream.filter((event) => event.type === "config.updated"),
|
||||||
|
Stream.runForEach(() =>
|
||||||
|
config.entries().pipe(
|
||||||
|
Effect.tap((entries) => Effect.sync(() => (loaded.entries = entries))),
|
||||||
|
Effect.andThen(image.reload()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Effect.forkScoped({ startImmediately: true }),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
})
|
||||||
+33
-17
@@ -2,8 +2,8 @@ export * as Image from "./image.js"
|
|||||||
|
|
||||||
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { Config } from "./config.js"
|
|
||||||
import { FileSystem } from "./filesystem.js"
|
import { FileSystem } from "./filesystem.js"
|
||||||
|
import { State } from "./state.js"
|
||||||
|
|
||||||
export class ResizerUnavailableError extends Schema.TaggedErrorClass<ResizerUnavailableError>()(
|
export class ResizerUnavailableError extends Schema.TaggedErrorClass<ResizerUnavailableError>()(
|
||||||
"Image.ResizerUnavailableError",
|
"Image.ResizerUnavailableError",
|
||||||
@@ -32,7 +32,18 @@ export class SizeError extends Schema.TaggedErrorClass<SizeError>()("Image.SizeE
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export type Limits = {
|
||||||
|
autoResize: boolean
|
||||||
|
maxWidth: number
|
||||||
|
maxHeight: number
|
||||||
|
maxBase64Bytes: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Draft = {
|
||||||
|
configure: (limits: Partial<Limits>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Interface extends State.Transformable<Draft> {
|
||||||
readonly normalize: (
|
readonly normalize: (
|
||||||
resource: string,
|
resource: string,
|
||||||
content: FileSystem.Content & { readonly encoding: "base64" },
|
content: FileSystem.Content & { readonly encoding: "base64" },
|
||||||
@@ -47,7 +58,23 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Im
|
|||||||
const layer = Layer.effect(
|
const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const config = yield* Config.Service
|
const state = State.create<Limits, Draft>({
|
||||||
|
name: "image",
|
||||||
|
initial: () => ({
|
||||||
|
autoResize: true,
|
||||||
|
maxWidth: 2_000,
|
||||||
|
maxHeight: 2_000,
|
||||||
|
maxBase64Bytes: 5 * 1024 * 1024,
|
||||||
|
}),
|
||||||
|
draft: (draft) => ({
|
||||||
|
configure: (limits) => {
|
||||||
|
if (limits.autoResize !== undefined) draft.autoResize = limits.autoResize
|
||||||
|
if (limits.maxWidth !== undefined) draft.maxWidth = limits.maxWidth
|
||||||
|
if (limits.maxHeight !== undefined) draft.maxHeight = limits.maxHeight
|
||||||
|
if (limits.maxBase64Bytes !== undefined) draft.maxBase64Bytes = limits.maxBase64Bytes
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
const loadAdapter = yield* Effect.cached(
|
const loadAdapter = yield* Effect.cached(
|
||||||
Effect.tryPromise({
|
Effect.tryPromise({
|
||||||
try: () => import("./image/photon.js"),
|
try: () => import("./image/photon.js"),
|
||||||
@@ -58,22 +85,11 @@ const layer = Layer.effect(
|
|||||||
resource: string,
|
resource: string,
|
||||||
content: FileSystem.Content & { readonly encoding: "base64" },
|
content: FileSystem.Content & { readonly encoding: "base64" },
|
||||||
) {
|
) {
|
||||||
const image = Object.assign(
|
|
||||||
{},
|
|
||||||
...(yield* config.entries()).flatMap((entry) =>
|
|
||||||
entry.type === "document" && entry.info.media?.image ? [entry.info.media.image] : [],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
const normalize = yield* loadAdapter
|
const normalize = yield* loadAdapter
|
||||||
return yield* normalize(resource, content, {
|
return yield* normalize(resource, content, state.get())
|
||||||
autoResize: image.auto_resize ?? true,
|
|
||||||
maxWidth: image.max_width ?? 2_000,
|
|
||||||
maxHeight: image.max_height ?? 2_000,
|
|
||||||
maxBase64Bytes: image.max_base64_bytes ?? 5 * 1024 * 1024,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
return Service.of({ normalize })
|
return Service.of({ transform: state.transform, reload: state.reload, normalize })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const node = makeLocationNode({ service: Service, layer, deps: [Config.node] })
|
export const node = makeLocationNode({ service: Service, layer, deps: [] })
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Config } from "../config.js"
|
|||||||
import { Credential } from "../credential.js"
|
import { Credential } from "../credential.js"
|
||||||
import { ConfigAgentPlugin } from "../config/plugin/agent.js"
|
import { ConfigAgentPlugin } from "../config/plugin/agent.js"
|
||||||
import { ConfigCommandPlugin } from "../config/plugin/command.js"
|
import { ConfigCommandPlugin } from "../config/plugin/command.js"
|
||||||
|
import { ConfigImagePlugin } from "../config/plugin/image.js"
|
||||||
import { ConfigInstructionPlugin } from "../config/plugin/instruction.js"
|
import { ConfigInstructionPlugin } from "../config/plugin/instruction.js"
|
||||||
import { ConfigProviderPlugin } from "../config/plugin/provider.js"
|
import { ConfigProviderPlugin } from "../config/plugin/provider.js"
|
||||||
import { ConfigPolicyPlugin } from "../config/plugin/policy.js"
|
import { ConfigPolicyPlugin } from "../config/plugin/policy.js"
|
||||||
@@ -224,6 +225,7 @@ const post = [
|
|||||||
ConfigReferencePlugin.Plugin,
|
ConfigReferencePlugin.Plugin,
|
||||||
ConfigAgentPlugin.Plugin,
|
ConfigAgentPlugin.Plugin,
|
||||||
ConfigCommandPlugin.Plugin,
|
ConfigCommandPlugin.Plugin,
|
||||||
|
ConfigImagePlugin.Plugin,
|
||||||
ConfigSkillPlugin.Plugin,
|
ConfigSkillPlugin.Plugin,
|
||||||
ConfigProviderPlugin.Plugin,
|
ConfigProviderPlugin.Plugin,
|
||||||
ConfigWebSearchPlugin.Plugin,
|
ConfigWebSearchPlugin.Plugin,
|
||||||
|
|||||||
@@ -528,7 +528,7 @@ export const createLLMEventPublisher = (bus: Pick<Bus.Interface, "publish">, inp
|
|||||||
return
|
return
|
||||||
case "provider-error":
|
case "provider-error":
|
||||||
providerFailed = true
|
providerFailed = true
|
||||||
yield* failAssistant({ type: "provider.unknown", message: event.message, data: event.data })
|
yield* failAssistant({ type: "provider.unknown", message: event.message })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -61,11 +61,5 @@ export function toSessionError(cause: unknown): SessionError.Error {
|
|||||||
function providerError(type: string, reason: AIError["reason"]): SessionError.Error {
|
function providerError(type: string, reason: AIError["reason"]): SessionError.Error {
|
||||||
const status =
|
const status =
|
||||||
("http" in reason ? reason.http?.response?.status : undefined) ?? ("status" in reason ? reason.status : undefined)
|
("http" in reason ? reason.http?.response?.status : undefined) ?? ("status" in reason ? reason.status : undefined)
|
||||||
const data = "data" in reason ? reason.data : reason._tag === "InvalidProviderOutput" ? reason.raw : undefined
|
return { type, message: reason.message, ...(status === undefined ? {} : { status }) }
|
||||||
return {
|
|
||||||
type,
|
|
||||||
message: reason.message,
|
|
||||||
...(status === undefined ? {} : { status }),
|
|
||||||
...(data === undefined ? {} : { data }),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -487,7 +487,6 @@ it.effect("derives status and code when the AI SDK error message is empty", () =
|
|||||||
expect(projected.type).toBe("provider.invalid-request")
|
expect(projected.type).toBe("provider.invalid-request")
|
||||||
expect(projected.status).toBe(404)
|
expect(projected.status).toBe(404)
|
||||||
expect(projected.message).not.toBe("")
|
expect(projected.message).not.toBe("")
|
||||||
expect(projected.data).toEqual({ error: { message: "", code: "not_found" } })
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -505,7 +504,6 @@ it.effect("preserves complete HTTP context on AI SDK call errors", () =>
|
|||||||
expect(http?.response?.status).toBe(404)
|
expect(http?.response?.status).toBe(404)
|
||||||
expect(http?.response?.headers["authorization"]).toBe("Bearer secret-token")
|
expect(http?.response?.headers["authorization"]).toBe("Bearer secret-token")
|
||||||
expect(http?.body).toBe('{"error":{"message":"","code":"not_found"}}')
|
expect(http?.body).toBe('{"error":{"message":"","code":"not_found"}}')
|
||||||
expect(error.reason).toMatchObject({ data: '{"error":{"message":"","code":"not_found"}}' })
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { describe, expect } from "bun:test"
|
||||||
|
import { Bus } from "@opencode-ai/core/bus"
|
||||||
|
import { Config } from "@opencode-ai/core/config"
|
||||||
|
import { ConfigImagePlugin } from "@opencode-ai/core/config/plugin/image"
|
||||||
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
|
import { Image } from "@opencode-ai/core/image"
|
||||||
|
import { Plugin } from "@opencode-ai/core/plugin"
|
||||||
|
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||||
|
import { Document, Event, Info, type Entry } from "@opencode-ai/schema/config"
|
||||||
|
import { Effect, Layer, Schema } from "effect"
|
||||||
|
import { testEffect } from "../lib/effect"
|
||||||
|
import { PluginTestLayer } from "../plugin/fixture"
|
||||||
|
|
||||||
|
const it = testEffect(Layer.merge(PluginTestLayer, AppNodeBuilder.build(Image.node)))
|
||||||
|
const decode = Schema.decodeUnknownSync(Info)
|
||||||
|
const content = {
|
||||||
|
uri: "file:///pixel.png",
|
||||||
|
content: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
|
||||||
|
encoding: "base64" as const,
|
||||||
|
mime: "image/png",
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ConfigImagePlugin.Plugin", () => {
|
||||||
|
it.live("merges image limits and reloads changed config", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const image = yield* Image.Service
|
||||||
|
const bus = yield* Bus.Service
|
||||||
|
const config = yield* Config.Test
|
||||||
|
const plugins = yield* Plugin.Service
|
||||||
|
yield* ConfigImagePlugin.Plugin.effect(yield* PluginHost.make(plugins))
|
||||||
|
|
||||||
|
expect(yield* limits(image)).toEqual({ maxWidth: 1_200, maxHeight: 900, maxBytes: 1 })
|
||||||
|
|
||||||
|
yield* config.setEntries([document({ auto_resize: false, max_width: 700, max_base64_bytes: 1 })])
|
||||||
|
yield* bus.publish(Event.Updated, {})
|
||||||
|
yield* waitUntil(
|
||||||
|
limits(image).pipe(
|
||||||
|
Effect.map((current) => current.maxWidth === 700 && current.maxHeight === 2_000 && current.maxBytes === 1),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}).pipe(
|
||||||
|
Effect.provide(
|
||||||
|
Config.testLayer([
|
||||||
|
document({ auto_resize: false, max_width: 1_200 }),
|
||||||
|
document({ max_height: 900, max_base64_bytes: 1 }),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function document(image: NonNullable<typeof Info.Encoded.media>["image"]): Entry {
|
||||||
|
return new Document({ type: "document", info: decode({ media: { image } }) })
|
||||||
|
}
|
||||||
|
|
||||||
|
const limits = Effect.fnUntraced(function* (image: Image.Interface) {
|
||||||
|
const error = yield* image.normalize("pixel.png", content).pipe(Effect.flip, Effect.orDie)
|
||||||
|
if (error._tag !== "Image.SizeError") return yield* Effect.die(error)
|
||||||
|
return { maxWidth: error.maxWidth, maxHeight: error.maxHeight, maxBytes: error.maxBytes }
|
||||||
|
})
|
||||||
|
|
||||||
|
const waitUntil = Effect.fnUntraced(function* (condition: Effect.Effect<boolean>) {
|
||||||
|
for (let attempt = 0; attempt < 200; attempt++) {
|
||||||
|
if (yield* condition) return
|
||||||
|
yield* Effect.sleep("10 millis")
|
||||||
|
}
|
||||||
|
yield* Effect.die(new Error("Timed out waiting for image config reload"))
|
||||||
|
})
|
||||||
@@ -61,24 +61,6 @@ describe("toSessionError", () => {
|
|||||||
).type,
|
).type,
|
||||||
).toBe("provider.no-route")
|
).toBe("provider.no-route")
|
||||||
expect(toSessionError(llm(new UnknownProviderReason({ message: "unknown" }))).type).toBe("provider.unknown")
|
expect(toSessionError(llm(new UnknownProviderReason({ message: "unknown" }))).type).toBe("provider.unknown")
|
||||||
expect(toSessionError(llm(new InvalidProviderOutputReason({ message: "malformed", raw: "not-json" })))).toEqual({
|
|
||||||
type: "provider.invalid-output",
|
|
||||||
message: "malformed",
|
|
||||||
data: "not-json",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test("preserves provider error data", () => {
|
|
||||||
const data = {
|
|
||||||
type: "error",
|
|
||||||
sequence_number: 2,
|
|
||||||
error: { type: "server_error", code: null, message: null },
|
|
||||||
}
|
|
||||||
expect(toSessionError(llm(new UnknownProviderReason({ message: "stream error", data })))).toEqual({
|
|
||||||
type: "provider.unknown",
|
|
||||||
message: "stream error",
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("preserves the permission rejection type without exposing internal fields", () => {
|
test("preserves the permission rejection type without exposing internal fields", () => {
|
||||||
|
|||||||
@@ -925,7 +925,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
yield* admit(session, "Second prompt")
|
yield* admit(session, "Second prompt")
|
||||||
const titleFailed = yield* Deferred.make<void>()
|
const titleFailed = yield* Deferred.make<void>()
|
||||||
yield* TestLLM.push(
|
yield* TestLLM.push(
|
||||||
Stream.make(LLMEvent.providerError({ message: "Title provider unavailable", data: {} })).pipe(
|
Stream.make(LLMEvent.providerError({ message: "Title provider unavailable" })).pipe(
|
||||||
Stream.ensuring(Deferred.succeed(titleFailed, undefined)),
|
Stream.ensuring(Deferred.succeed(titleFailed, undefined)),
|
||||||
),
|
),
|
||||||
TestLLM.text("Recovered", "text-recovered"),
|
TestLLM.text("Recovered", "text-recovered"),
|
||||||
@@ -2179,7 +2179,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
yield* TestLLM.push(TestLLM.text("Earlier answer", "text-manual-provider-history"))
|
yield* TestLLM.push(TestLLM.text("Earlier answer", "text-manual-provider-history"))
|
||||||
yield* runPrompt(session, "Earlier question")
|
yield* runPrompt(session, "Earlier question")
|
||||||
|
|
||||||
yield* TestLLM.push([LLMEvent.providerError({ message: "summary unavailable", data: {} })])
|
yield* TestLLM.push([LLMEvent.providerError({ message: "summary unavailable" })])
|
||||||
const compaction = yield* session.compact({ sessionID })
|
const compaction = yield* session.compact({ sessionID })
|
||||||
yield* session.resume(sessionID)
|
yield* session.resume(sessionID)
|
||||||
|
|
||||||
@@ -2337,7 +2337,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
currentModel = compactModel
|
currentModel = compactModel
|
||||||
requests.length = 0
|
requests.length = 0
|
||||||
yield* TestLLM.push(
|
yield* TestLLM.push(
|
||||||
[LLMEvent.providerError({ message: "Unsupported parameter: max_output_tokens", data: {} })],
|
[LLMEvent.providerError({ message: "Unsupported parameter: max_output_tokens" })],
|
||||||
TestLLM.text("Must not run", "text-after-failed-compaction"),
|
TestLLM.text("Must not run", "text-after-failed-compaction"),
|
||||||
)
|
)
|
||||||
yield* admit(session, "Recent exact request ".repeat(180))
|
yield* admit(session, "Recent exact request ".repeat(180))
|
||||||
@@ -2362,7 +2362,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
yield* TestLLM.push(
|
yield* TestLLM.push(
|
||||||
[
|
[
|
||||||
LLMEvent.stepStart({ index: 0 }),
|
LLMEvent.stepStart({ index: 0 }),
|
||||||
LLMEvent.providerError({ message: "prompt too long", data: {}, classification: "context-overflow" }),
|
LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" }),
|
||||||
],
|
],
|
||||||
TestLLM.text("## Objective\n- Recover overflow", "text-summary"),
|
TestLLM.text("## Objective\n- Recover overflow", "text-summary"),
|
||||||
TestLLM.text("Recovered", "text-final"),
|
TestLLM.text("Recovered", "text-final"),
|
||||||
@@ -2389,7 +2389,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
const session = yield* setupOverflowRecovery
|
const session = yield* setupOverflowRecovery
|
||||||
currentModel = model
|
currentModel = model
|
||||||
yield* TestLLM.push(
|
yield* TestLLM.push(
|
||||||
[LLMEvent.providerError({ message: "prompt too long", data: {}, classification: "context-overflow" })],
|
[LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })],
|
||||||
TestLLM.text("## Objective\n- Recover unknown limit", "text-summary-unknown-limit"),
|
TestLLM.text("## Objective\n- Recover unknown limit", "text-summary-unknown-limit"),
|
||||||
TestLLM.text("Recovered", "text-final-unknown-limit"),
|
TestLLM.text("Recovered", "text-final-unknown-limit"),
|
||||||
)
|
)
|
||||||
@@ -2408,7 +2408,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
const session = yield* setupOverflowRecovery
|
const session = yield* setupOverflowRecovery
|
||||||
currentModel = undersizedContextModel
|
currentModel = undersizedContextModel
|
||||||
yield* TestLLM.push(
|
yield* TestLLM.push(
|
||||||
[LLMEvent.providerError({ message: "prompt too long", data: {}, classification: "context-overflow" })],
|
[LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })],
|
||||||
TestLLM.text("## Objective\n- Recover undersized limit", "text-summary-undersized-limit"),
|
TestLLM.text("## Objective\n- Recover undersized limit", "text-summary-undersized-limit"),
|
||||||
TestLLM.text("Recovered", "text-final-undersized-limit"),
|
TestLLM.text("Recovered", "text-final-undersized-limit"),
|
||||||
)
|
)
|
||||||
@@ -2427,7 +2427,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
const session = yield* setupOverflowRecovery
|
const session = yield* setupOverflowRecovery
|
||||||
const overflow = () => [
|
const overflow = () => [
|
||||||
LLMEvent.stepStart({ index: 0 }),
|
LLMEvent.stepStart({ index: 0 }),
|
||||||
LLMEvent.providerError({ message: "prompt too long", data: {}, classification: "context-overflow" }),
|
LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" }),
|
||||||
]
|
]
|
||||||
yield* TestLLM.push(overflow(), TestLLM.text("## Objective\n- Recover once", "text-summary"), overflow())
|
yield* TestLLM.push(overflow(), TestLLM.text("## Objective\n- Recover once", "text-summary"), overflow())
|
||||||
yield* admit(session, "Continue")
|
yield* admit(session, "Continue")
|
||||||
@@ -2474,8 +2474,8 @@ describe("SessionRunnerLLM", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const session = yield* setupOverflowRecovery
|
const session = yield* setupOverflowRecovery
|
||||||
yield* TestLLM.push(
|
yield* TestLLM.push(
|
||||||
[LLMEvent.providerError({ message: "prompt too long", data: {}, classification: "context-overflow" })],
|
[LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })],
|
||||||
[LLMEvent.providerError({ message: "summary unavailable", data: {} })],
|
[LLMEvent.providerError({ message: "summary unavailable" })],
|
||||||
)
|
)
|
||||||
yield* admit(session, "Continue")
|
yield* admit(session, "Continue")
|
||||||
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("prompt too long")
|
expect((yield* session.resume(sessionID).pipe(Effect.flip)).message).toBe("prompt too long")
|
||||||
@@ -2502,7 +2502,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const session = yield* setupOverflowRecovery
|
const session = yield* setupOverflowRecovery
|
||||||
yield* TestLLM.push(
|
yield* TestLLM.push(
|
||||||
[LLMEvent.providerError({ message: "prompt too long", data: {}, classification: "context-overflow" })],
|
[LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" })],
|
||||||
TestLLM.text("## Objective\n- Interrupted", "text-summary"),
|
TestLLM.text("## Objective\n- Interrupted", "text-summary"),
|
||||||
)
|
)
|
||||||
const first = yield* TestLLM.gate
|
const first = yield* TestLLM.gate
|
||||||
@@ -4078,10 +4078,9 @@ describe("SessionRunnerLLM", () => {
|
|||||||
it.effect("projects provider errors as terminal assistant step failures", () =>
|
it.effect("projects provider errors as terminal assistant step failures", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const session = yield* setup
|
const session = yield* setup
|
||||||
const data = { type: "error", error: { type: "server_error" } }
|
|
||||||
yield* TestLLM.push([
|
yield* TestLLM.push([
|
||||||
LLMEvent.stepStart({ index: 0 }),
|
LLMEvent.stepStart({ index: 0 }),
|
||||||
LLMEvent.providerError({ message: "Provider unavailable", data }),
|
LLMEvent.providerError({ message: "Provider unavailable" }),
|
||||||
])
|
])
|
||||||
|
|
||||||
expect((yield* runPrompt(session, "Fail durably").pipe(Effect.flip)).message).toBe("Provider unavailable")
|
expect((yield* runPrompt(session, "Fail durably").pipe(Effect.flip)).message).toBe("Provider unavailable")
|
||||||
@@ -4089,11 +4088,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
expect(requests).toHaveLength(1)
|
expect(requests).toHaveLength(1)
|
||||||
expect(yield* session.context(sessionID)).toMatchObject([
|
expect(yield* session.context(sessionID)).toMatchObject([
|
||||||
{ type: "user", text: "Fail durably" },
|
{ type: "user", text: "Fail durably" },
|
||||||
{
|
{ type: "assistant", finish: "error", error: { type: "provider.unknown", message: "Provider unavailable" } },
|
||||||
type: "assistant",
|
|
||||||
finish: "error",
|
|
||||||
error: { type: "provider.unknown", message: "Provider unavailable", data },
|
|
||||||
},
|
|
||||||
])
|
])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -4101,7 +4096,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
it.effect("projects provider errors emitted before assistant step start", () =>
|
it.effect("projects provider errors emitted before assistant step start", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const session = yield* setup
|
const session = yield* setup
|
||||||
yield* TestLLM.push([LLMEvent.providerError({ message: "Provider unavailable", data: {} })])
|
yield* TestLLM.push([LLMEvent.providerError({ message: "Provider unavailable" })])
|
||||||
|
|
||||||
expect((yield* runPrompt(session, "Fail before step").pipe(Effect.flip)).message).toBe("Provider unavailable")
|
expect((yield* runPrompt(session, "Fail before step").pipe(Effect.flip)).message).toBe("Provider unavailable")
|
||||||
|
|
||||||
@@ -4188,7 +4183,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
LLMEvent.textStart({ id: "text-partial" }),
|
LLMEvent.textStart({ id: "text-partial" }),
|
||||||
LLMEvent.textDelta({ id: "text-partial", text: "Partial" }),
|
LLMEvent.textDelta({ id: "text-partial", text: "Partial" }),
|
||||||
LLMEvent.textEnd({ id: "text-partial" }),
|
LLMEvent.textEnd({ id: "text-partial" }),
|
||||||
LLMEvent.providerError({ message: "prompt too long", data: {}, classification: "context-overflow" }),
|
LLMEvent.providerError({ message: "prompt too long", classification: "context-overflow" }),
|
||||||
])
|
])
|
||||||
expect((yield* runPrompt(session, "Fail after output").pipe(Effect.flip)).message).toBe("prompt too long")
|
expect((yield* runPrompt(session, "Fail after output").pipe(Effect.flip)).message).toBe("prompt too long")
|
||||||
|
|
||||||
@@ -4963,7 +4958,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
yield* TestLLM.push([
|
yield* TestLLM.push([
|
||||||
LLMEvent.stepStart({ index: 0 }),
|
LLMEvent.stepStart({ index: 0 }),
|
||||||
LLMEvent.toolCall({ id: "call-before-provider-error", name: "echo", input: { text: "settled" } }),
|
LLMEvent.toolCall({ id: "call-before-provider-error", name: "echo", input: { text: "settled" } }),
|
||||||
LLMEvent.providerError({ message: "Provider unavailable", data: {} }),
|
LLMEvent.providerError({ message: "Provider unavailable" }),
|
||||||
])
|
])
|
||||||
|
|
||||||
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
const run = yield* session.resume(sessionID).pipe(Effect.forkChild)
|
||||||
@@ -4990,7 +4985,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
yield* TestLLM.push([
|
yield* TestLLM.push([
|
||||||
LLMEvent.stepStart({ index: 0 }),
|
LLMEvent.stepStart({ index: 0 }),
|
||||||
hostedCall("call-hosted-provider-error", "effect"),
|
hostedCall("call-hosted-provider-error", "effect"),
|
||||||
LLMEvent.providerError({ message: "Provider unavailable", data: {} }),
|
LLMEvent.providerError({ message: "Provider unavailable" }),
|
||||||
])
|
])
|
||||||
|
|
||||||
expect((yield* runPrompt(session, "Fail hosted tool durably").pipe(Effect.flip)).message).toBe(
|
expect((yield* runPrompt(session, "Fail hosted tool durably").pipe(Effect.flip)).message).toBe(
|
||||||
@@ -5022,7 +5017,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
yield* TestLLM.push([
|
yield* TestLLM.push([
|
||||||
LLMEvent.stepStart({ index: 0 }),
|
LLMEvent.stepStart({ index: 0 }),
|
||||||
LLMEvent.toolCall({ id: "call-defect-provider-error", name: "defect", input: {} }),
|
LLMEvent.toolCall({ id: "call-defect-provider-error", name: "defect", input: {} }),
|
||||||
LLMEvent.providerError({ message: "Provider unavailable", data: {} }),
|
LLMEvent.providerError({ message: "Provider unavailable" }),
|
||||||
])
|
])
|
||||||
|
|
||||||
expect((yield* runPrompt(session, "Defect while provider fails").pipe(Effect.flip)).message).toBe(
|
expect((yield* runPrompt(session, "Defect while provider fails").pipe(Effect.flip)).message).toBe(
|
||||||
@@ -5049,7 +5044,7 @@ describe("SessionRunnerLLM", () => {
|
|||||||
yield* TestLLM.push([
|
yield* TestLLM.push([
|
||||||
LLMEvent.stepStart({ index: 0 }),
|
LLMEvent.stepStart({ index: 0 }),
|
||||||
LLMEvent.toolCall({ id: "call-store-provider-error", name: "storefail", input: {} }),
|
LLMEvent.toolCall({ id: "call-store-provider-error", name: "storefail", input: {} }),
|
||||||
LLMEvent.providerError({ message: "Provider unavailable", data: {} }),
|
LLMEvent.providerError({ message: "Provider unavailable" }),
|
||||||
])
|
])
|
||||||
|
|
||||||
expect(yield* session.resume(sessionID).pipe(Effect.exit)).toMatchObject({
|
expect(yield* session.resume(sessionID).pipe(Effect.exit)).toMatchObject({
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ it.effect("retries after a failed title request", () =>
|
|||||||
yield* insertSession(sessionID)
|
yield* insertSession(sessionID)
|
||||||
yield* prompt(sessionID, "Retry this title")
|
yield* prompt(sessionID, "Retry this title")
|
||||||
const title = yield* SessionTitle.Service
|
const title = yield* SessionTitle.Service
|
||||||
titleStream = () => Stream.make(LLMEvent.providerError({ message: "Provider unavailable", data: {} }))
|
titleStream = () => Stream.make(LLMEvent.providerError({ message: "Provider unavailable" }))
|
||||||
|
|
||||||
yield* title.generateForFirstPrompt(sessionID)
|
yield* title.generateForFirstPrompt(sessionID)
|
||||||
titleStream = successfulTitle
|
titleStream = successfulTitle
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { beforeEach, describe, expect } from "bun:test"
|
import { beforeEach, describe, expect } from "bun:test"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { Effect, Exit, Layer, Stream } from "effect"
|
import { Effect, Exit, Layer } from "effect"
|
||||||
import { Config } from "@opencode-ai/core/config"
|
import { Config } from "@opencode-ai/core/config"
|
||||||
import { Document, Info } from "@opencode-ai/schema/config"
|
|
||||||
import { ConfigMedia } from "@opencode-ai/schema/config/media"
|
|
||||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||||
@@ -90,7 +88,7 @@ const permission = permissionLayer({
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
const config = Config.testLayer()
|
const config = Config.testLayer()
|
||||||
const imageLayer = AppNodeBuilder.build(Image.node, [[Config.node, config]])
|
const imageLayer = AppNodeBuilder.build(Image.node)
|
||||||
const testFileSystem = Layer.effect(
|
const testFileSystem = Layer.effect(
|
||||||
FSUtil.Service,
|
FSUtil.Service,
|
||||||
FSUtil.Service.use((fs) =>
|
FSUtil.Service.use((fs) =>
|
||||||
@@ -130,10 +128,9 @@ const mutation = Layer.succeed(
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const unavailableImage = Layer.succeed(
|
const unavailableImage = Layer.mock(Image.Service, {
|
||||||
Image.Service,
|
normalize: () => Effect.fail(new Image.ResizerUnavailableError()),
|
||||||
Image.Service.of({ normalize: () => Effect.fail(new Image.ResizerUnavailableError()) }),
|
})
|
||||||
)
|
|
||||||
const readLayer = (imageLayer: Layer.Layer<Image.Service>) =>
|
const readLayer = (imageLayer: Layer.Layer<Image.Service>) =>
|
||||||
Layer.mergeAll(
|
Layer.mergeAll(
|
||||||
AppNodeBuilder.build(LayerNode.group([Tool.node, readToolNode]), [
|
AppNodeBuilder.build(LayerNode.group([Tool.node, readToolNode]), [
|
||||||
@@ -146,8 +143,9 @@ const readLayer = (imageLayer: Layer.Layer<Image.Service>) =>
|
|||||||
[Location.node, locationLayer],
|
[Location.node, locationLayer],
|
||||||
[Global.node, Global.layerWith({ data: Global.Path.data })],
|
[Global.node, Global.layerWith({ data: Global.Path.data })],
|
||||||
]),
|
]),
|
||||||
// Merge by reference so Config.Test resolves to the memoized instance.
|
// Merge by reference so Config.Test and Image.Service resolve to the memoized instances.
|
||||||
config,
|
config,
|
||||||
|
imageLayer,
|
||||||
)
|
)
|
||||||
const it = testEffect(readLayer(imageLayer))
|
const it = testEffect(readLayer(imageLayer))
|
||||||
const itWithoutResizer = testEffect(readLayer(unavailableImage))
|
const itWithoutResizer = testEffect(readLayer(unavailableImage))
|
||||||
@@ -384,17 +382,8 @@ describe("ReadTool", () => {
|
|||||||
encoding: "base64",
|
encoding: "base64",
|
||||||
mime: "image/png",
|
mime: "image/png",
|
||||||
}
|
}
|
||||||
const configTest = yield* Config.Test
|
const image = yield* Image.Service
|
||||||
yield* configTest.setEntries([
|
yield* image.transform((draft) => draft.configure({ autoResize: false, maxWidth: 4 }))
|
||||||
new Document({
|
|
||||||
type: "document",
|
|
||||||
info: new Info({
|
|
||||||
media: new ConfigMedia.Info({
|
|
||||||
image: new ConfigMedia.Image({ auto_resize: false, max_width: 4 }),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
const registry = yield* Tool.Service
|
const registry = yield* Tool.Service
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@@ -427,15 +416,8 @@ describe("ReadTool", () => {
|
|||||||
encoding: "base64",
|
encoding: "base64",
|
||||||
mime: "image/png",
|
mime: "image/png",
|
||||||
}
|
}
|
||||||
const configTest = yield* Config.Test
|
const image = yield* Image.Service
|
||||||
yield* configTest.setEntries([
|
yield* image.transform((draft) => draft.configure({ maxWidth: 4 }))
|
||||||
new Document({
|
|
||||||
type: "document",
|
|
||||||
info: new Info({
|
|
||||||
media: new ConfigMedia.Info({ image: new ConfigMedia.Image({ max_width: 4 }) }),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
const registry = yield* Tool.Service
|
const registry = yield* Tool.Service
|
||||||
const result = yield* executeTool(registry, {
|
const result = yield* executeTool(registry, {
|
||||||
sessionID,
|
sessionID,
|
||||||
@@ -466,17 +448,8 @@ describe("ReadTool", () => {
|
|||||||
encoding: "base64",
|
encoding: "base64",
|
||||||
mime: "image/png",
|
mime: "image/png",
|
||||||
}
|
}
|
||||||
const configTest = yield* Config.Test
|
const image = yield* Image.Service
|
||||||
yield* configTest.setEntries([
|
yield* image.transform((draft) => draft.configure({ maxBase64Bytes: 1 }))
|
||||||
new Document({
|
|
||||||
type: "document",
|
|
||||||
info: new Info({
|
|
||||||
media: new ConfigMedia.Info({
|
|
||||||
image: new ConfigMedia.Image({ max_base64_bytes: 1 }),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
const registry = yield* Tool.Service
|
const registry = yield* Tool.Service
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -8,5 +8,4 @@ export const Error = Schema.Struct({
|
|||||||
type: Schema.String,
|
type: Schema.String,
|
||||||
message: Schema.String,
|
message: Schema.String,
|
||||||
status: Schema.Int.check(Schema.isBetween({ minimum: 100, maximum: 599 })).pipe(optional),
|
status: Schema.Int.check(Schema.isBetween({ minimum: 100, maximum: 599 })).pipe(optional),
|
||||||
data: Schema.Json.pipe(optional),
|
|
||||||
}).annotate({ identifier: "Session.StructuredError" })
|
}).annotate({ identifier: "Session.StructuredError" })
|
||||||
|
|||||||
@@ -12,11 +12,6 @@ describe("SessionError", () => {
|
|||||||
const values: SessionError.Error[] = [
|
const values: SessionError.Error[] = [
|
||||||
{ type: "provider.rate-limit", message: "Slow down" },
|
{ type: "provider.rate-limit", message: "Slow down" },
|
||||||
{ type: "provider.auth", message: "Authentication failed" },
|
{ type: "provider.auth", message: "Authentication failed" },
|
||||||
{
|
|
||||||
type: "provider.unknown",
|
|
||||||
message: "Stream error",
|
|
||||||
data: { type: "error", error: { type: "server_error" } },
|
|
||||||
},
|
|
||||||
{ type: "provider.future-condition", message: "A future provider failure" },
|
{ type: "provider.future-condition", message: "A future provider failure" },
|
||||||
{ type: "unknown", message: "Unexpected" },
|
{ type: "unknown", message: "Unexpected" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user