mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-18 13:49:25 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 73ee68b6a2 |
+30
-17
@@ -129,20 +129,6 @@ function prepareOptions(model: ModelV2.Info, pkg: string) {
|
|||||||
if (abortSignals.length === 1) opts.signal = abortSignals[0]
|
if (abortSignals.length === 1) opts.signal = abortSignals[0]
|
||||||
if (abortSignals.length > 1) opts.signal = AbortSignal.any(abortSignals)
|
if (abortSignals.length > 1) opts.signal = AbortSignal.any(abortSignals)
|
||||||
|
|
||||||
if (
|
|
||||||
(pkg === "@ai-sdk/openai" || pkg === "@ai-sdk/azure" || pkg === "@ai-sdk/amazon-bedrock/mantle") &&
|
|
||||||
opts.body &&
|
|
||||||
opts.method === "POST"
|
|
||||||
) {
|
|
||||||
const body = JSON.parse(opts.body as string)
|
|
||||||
if (body.store !== true && Array.isArray(body.input)) {
|
|
||||||
for (const item of body.input) {
|
|
||||||
if ("id" in item) delete item.id
|
|
||||||
}
|
|
||||||
opts.body = JSON.stringify(body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof opts.body === "string" && model.body !== undefined) {
|
if (typeof opts.body === "string" && model.body !== undefined) {
|
||||||
const decoded = Option.getOrUndefined(Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(opts.body))
|
const decoded = Option.getOrUndefined(Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(opts.body))
|
||||||
if (Schema.is(Schema.Record(Schema.String, Schema.Json))(decoded)) {
|
if (Schema.is(Schema.Record(Schema.String, Schema.Json))(decoded)) {
|
||||||
@@ -337,7 +323,7 @@ function modelFromLanguage(info: ModelV2.Info, language: LanguageModelV3) {
|
|||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
schema: Schema.Unknown,
|
schema: Schema.Unknown,
|
||||||
from: (request) => Effect.succeed(callOptions(request)),
|
from: (request) => Effect.succeed(callOptions(request, info)),
|
||||||
},
|
},
|
||||||
with: () => route,
|
with: () => route,
|
||||||
model: (input) => Model.make({ ...input, provider: "provider" in input ? input.provider : info.providerID, route }),
|
model: (input) => Model.make({ ...input, provider: "provider" in input ? input.provider : info.providerID, route }),
|
||||||
@@ -411,8 +397,11 @@ function mapBodyToProviderOptions(model: ModelV2.Info, packageName: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function callOptions(request: LLMRequest): LanguageModelV3CallOptions {
|
function callOptions(request: LLMRequest, model: ModelV2.Info): LanguageModelV3CallOptions {
|
||||||
return {
|
const packageName = ProviderV2.isAISDK(model.package) ? ProviderV2.packageName(model.package) : undefined
|
||||||
|
const optionKey = providerOptionKey(packageName, model.providerID)
|
||||||
|
const store = request.providerOptions?.[optionKey]?.store
|
||||||
|
const options: LanguageModelV3CallOptions = {
|
||||||
prompt: prompt(request),
|
prompt: prompt(request),
|
||||||
maxOutputTokens: request.generation?.maxTokens ?? request.model.route.defaults.limits?.output,
|
maxOutputTokens: request.generation?.maxTokens ?? request.model.route.defaults.limits?.output,
|
||||||
temperature: request.generation?.temperature,
|
temperature: request.generation?.temperature,
|
||||||
@@ -428,6 +417,30 @@ function callOptions(request: LLMRequest): LanguageModelV3CallOptions {
|
|||||||
headers: request.http?.headers,
|
headers: request.http?.headers,
|
||||||
providerOptions: providerOptions(request.providerOptions),
|
providerOptions: providerOptions(request.providerOptions),
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
store !== true &&
|
||||||
|
packageName !== undefined &&
|
||||||
|
["@ai-sdk/openai", "@ai-sdk/azure", "@ai-sdk/amazon-bedrock/mantle", "@ai-sdk/github-copilot"].includes(
|
||||||
|
packageName,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
options.prompt = options.prompt.map((message) => {
|
||||||
|
if (!Array.isArray(message.content)) return message
|
||||||
|
return {
|
||||||
|
...message,
|
||||||
|
content: message.content.map((part) => {
|
||||||
|
if (!("providerOptions" in part)) return part
|
||||||
|
const providerOptions = part.providerOptions
|
||||||
|
const metadata = providerOptions?.[optionKey]
|
||||||
|
if (metadata === undefined || !("itemId" in metadata)) return part
|
||||||
|
const next = { ...metadata }
|
||||||
|
delete next.itemId
|
||||||
|
return { ...part, providerOptions: { ...providerOptions, [optionKey]: next } }
|
||||||
|
}),
|
||||||
|
} as LanguageModelV3Message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
function prompt(request: LLMRequest): LanguageModelV3Prompt {
|
function prompt(request: LLMRequest): LanguageModelV3Prompt {
|
||||||
|
|||||||
@@ -237,10 +237,19 @@ export async function convertToOpenAIResponsesInput({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warnings.push({
|
const encryptedContent = providerOptions?.reasoningEncryptedContent
|
||||||
type: "other",
|
if (encryptedContent != null) {
|
||||||
message: `Non-OpenAI reasoning parts are not supported. Skipping reasoning part: ${JSON.stringify(part)}.`,
|
input.push({
|
||||||
})
|
type: "reasoning",
|
||||||
|
encrypted_content: encryptedContent,
|
||||||
|
summary: part.text.length > 0 ? [{ type: "summary_text", text: part.text }] : [],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
warnings.push({
|
||||||
|
type: "other",
|
||||||
|
message: `Non-OpenAI reasoning parts are not supported. Skipping reasoning part: ${JSON.stringify(part)}.`,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ export type OpenAIResponsesTool =
|
|||||||
|
|
||||||
export type OpenAIResponsesReasoning = {
|
export type OpenAIResponsesReasoning = {
|
||||||
type: "reasoning"
|
type: "reasoning"
|
||||||
id: string
|
id?: string
|
||||||
encrypted_content?: string | null
|
encrypted_content?: string | null
|
||||||
summary: Array<{
|
summary: Array<{
|
||||||
type: "summary_text"
|
type: "summary_text"
|
||||||
|
|||||||
@@ -270,6 +270,140 @@ it.effect("projects replay metadata onto AI SDK prompt parts", () =>
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("strips stale Azure item IDs before stateless AI SDK serialization", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
|
yield* aisdk.hook.sdk((event) => {
|
||||||
|
event.sdk = { languageModel: () => ({ provider: event.model.providerID }) }
|
||||||
|
})
|
||||||
|
|
||||||
|
const messages = [
|
||||||
|
Message.assistant([
|
||||||
|
{
|
||||||
|
type: "reasoning" as const,
|
||||||
|
text: "Think",
|
||||||
|
providerMetadata: {
|
||||||
|
azure: { itemId: "rs_123", reasoningEncryptedContent: "encrypted" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "tool-call" as const,
|
||||||
|
id: "call_1",
|
||||||
|
name: "lookup",
|
||||||
|
input: { query: "Effect" },
|
||||||
|
providerMetadata: { azure: { itemId: "fc_123", otherOption: "value" } },
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
const resolved = yield* aisdk.model(model("@ai-sdk/azure", { store: false }))
|
||||||
|
const prepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
|
||||||
|
LLM.request({
|
||||||
|
model: resolved,
|
||||||
|
messages,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(prepared.body.prompt).toEqual([
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "reasoning",
|
||||||
|
text: "Think",
|
||||||
|
providerOptions: { azure: { reasoningEncryptedContent: "encrypted" } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "tool-call",
|
||||||
|
toolCallId: "call_1",
|
||||||
|
toolName: "lookup",
|
||||||
|
input: { query: "Effect" },
|
||||||
|
providerExecuted: undefined,
|
||||||
|
providerOptions: { azure: { otherOption: "value" } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const stateful = yield* aisdk.model(model("@ai-sdk/azure", { store: true }))
|
||||||
|
const statefulPrepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
|
||||||
|
LLM.request({ model: stateful, messages }),
|
||||||
|
)
|
||||||
|
expect(statefulPrepared.body.prompt[0]).toMatchObject({
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{ providerOptions: { azure: { itemId: "rs_123" } } },
|
||||||
|
{ providerOptions: { azure: { itemId: "fc_123" } } },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const copilot = yield* aisdk.model(model("@ai-sdk/github-copilot", { store: false }))
|
||||||
|
const copilotPrepared = yield* LLMClient.prepare<LanguageModelV3CallOptions>(
|
||||||
|
LLM.request({
|
||||||
|
model: copilot,
|
||||||
|
messages: [
|
||||||
|
Message.assistant([
|
||||||
|
{
|
||||||
|
type: "reasoning",
|
||||||
|
text: "Think",
|
||||||
|
providerMetadata: {
|
||||||
|
copilot: { itemId: "rs_123", reasoningEncryptedContent: "encrypted" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(copilotPrepared.body.prompt[0]).toMatchObject({
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
providerOptions: { copilot: { reasoningEncryptedContent: "encrypted" } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("does not corrupt AI SDK item reference payloads", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const aisdk = yield* AISDK.Service
|
||||||
|
let sent: unknown
|
||||||
|
const server = yield* Effect.acquireRelease(
|
||||||
|
Effect.sync(() =>
|
||||||
|
Bun.serve({
|
||||||
|
port: 0,
|
||||||
|
async fetch(request) {
|
||||||
|
sent = await request.json()
|
||||||
|
return new Response(null, { status: 200 })
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
(server) => Effect.sync(() => server.stop(true)),
|
||||||
|
)
|
||||||
|
let wrapped: typeof fetch | undefined
|
||||||
|
yield* aisdk.hook.sdk((event) => {
|
||||||
|
wrapped = event.options.fetch
|
||||||
|
event.sdk = { languageModel: () => ({ provider: event.model.providerID }) }
|
||||||
|
})
|
||||||
|
|
||||||
|
yield* aisdk.language(model("@ai-sdk/azure"))
|
||||||
|
yield* Effect.promise(() =>
|
||||||
|
wrapped!(new URL("responses", server.url), {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
store: false,
|
||||||
|
input: [{ type: "item_reference", id: "fc_123" }],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(sent).toEqual({
|
||||||
|
store: false,
|
||||||
|
input: [{ type: "item_reference", id: "fc_123" }],
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.effect("emits malformed AI SDK tool input without executing it", () =>
|
it.effect("emits malformed AI SDK tool input without executing it", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const aisdk = yield* AISDK.Service
|
const aisdk = yield* AISDK.Service
|
||||||
|
|||||||
@@ -163,7 +163,35 @@ describe("convertToOpenAIResponsesInput", () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test("drops reasoning items with no copilot itemId and warns, as before", async () => {
|
test("preserves encrypted reasoning with no copilot itemId", async () => {
|
||||||
|
const { input, warnings } = await convertToOpenAIResponsesInput({
|
||||||
|
prompt: [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "reasoning",
|
||||||
|
text: "thinking...",
|
||||||
|
providerOptions: { copilot: { reasoningEncryptedContent: "enc_1" } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
systemMessageMode: "system",
|
||||||
|
store: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(warnings).toEqual([])
|
||||||
|
expect(input).toEqual([
|
||||||
|
{
|
||||||
|
type: "reasoning",
|
||||||
|
encrypted_content: "enc_1",
|
||||||
|
summary: [{ type: "summary_text", text: "thinking..." }],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("drops reasoning with neither a copilot itemId nor encrypted content", async () => {
|
||||||
const { input, warnings } = await convertToOpenAIResponsesInput({
|
const { input, warnings } = await convertToOpenAIResponsesInput({
|
||||||
prompt: [
|
prompt: [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user