@opencode-ai/ai
Advanced tools
@@ -1,2 +0,2 @@ | ||
| import { Effect, Schema } from "effect"; | ||
| import { Effect, Option, Schema } from "effect"; | ||
| import { HttpTransport } from "../route/transport/index.js"; | ||
@@ -627,2 +627,10 @@ import { Protocol } from "../route/protocol.js"; | ||
| }; | ||
| const decodeMessagePart = Schema.decodeUnknownOption(Schema.Union([OpenResponsesOutputText, Schema.Struct({ type: Schema.tag("refusal"), refusal: Schema.String })])); | ||
| const decodeSummaryPart = Schema.decodeUnknownOption(OpenResponsesReasoningSummaryText); | ||
| const decodeReasoningPart = Schema.decodeUnknownOption(Schema.Struct({ type: Schema.tag("reasoning_text"), text: Schema.String })); | ||
| const joinReasoningText = (parts) => { | ||
| if (!parts.some((part) => part !== undefined && part.length > 0)) | ||
| return undefined; | ||
| return parts.filter((part) => part !== undefined).join("\n\n"); | ||
| }; | ||
| export const outputItemID = (state, event) => event.output_index === undefined ? event.item_id : (state.outputItems[event.output_index] ?? event.item_id); | ||
@@ -825,15 +833,26 @@ const startReasoningSummaryPart = (state, itemID, index) => { | ||
| }); | ||
| const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (state, event) { | ||
| const item = event.item; | ||
| const onOutputItemDone = Effect.fn("OpenResponses.onOutputItemDone")(function* (state, item) { | ||
| if (!item) | ||
| return [state, NO_EVENTS]; | ||
| if (item.type === "message" && item.id !== undefined) { | ||
| const message = state.message?.id === item.id ? state.message : undefined; | ||
| const itemPhase = messagePhase(item.phase); | ||
| const phase = itemPhase === undefined && state.message?.id === item.id ? state.message.phase : itemPhase; | ||
| const phase = itemPhase === undefined ? message?.phase : itemPhase; | ||
| const parts = Array.isArray(item.content) ? item.content : []; | ||
| const content = []; | ||
| for (const part of parts) { | ||
| const decoded = Option.getOrUndefined(decodeMessagePart(part)); | ||
| if (!decoded) | ||
| continue; | ||
| content.push(decoded.type === "output_text" ? decoded.text : decoded.refusal); | ||
| } | ||
| const text = content.length > 0 ? content.join("") : undefined; | ||
| const metadata = providerMetadata(state, { itemId: item.id, ...(phase === undefined ? {} : { phase }) }); | ||
| const events = []; | ||
| const lifecycle = message && text ? Lifecycle.textStart(state.lifecycle, events, item.id, metadata) : state.lifecycle; | ||
| return [ | ||
| { | ||
| ...state, | ||
| lifecycle: Lifecycle.textEnd(state.lifecycle, events, item.id, providerMetadata(state, { itemId: item.id, ...(phase === undefined ? {} : { phase }) })), | ||
| message: state.message?.id === item.id ? undefined : state.message, | ||
| lifecycle: Lifecycle.textEnd(lifecycle, events, item.id, metadata, text), | ||
| message: message ? undefined : state.message, | ||
| }, | ||
@@ -888,11 +907,32 @@ events, | ||
| if (isReasoningItem(item)) { | ||
| if (state.reasoningItems[item.id]?.open === false) | ||
| return [state, NO_EVENTS]; | ||
| const metadata = reasoningMetadata(state, item); | ||
| const summaryParts = Array.isArray(item.summary) ? item.summary : []; | ||
| const summary = []; | ||
| for (const part of summaryParts) { | ||
| const decoded = Option.getOrUndefined(decodeSummaryPart(part)); | ||
| // Keep missing entries so the array still matches the provider's summary indexes. | ||
| summary.push(decoded?.text); | ||
| } | ||
| const reasoningParts = Array.isArray(item.content) ? item.content : []; | ||
| const content = []; | ||
| for (const part of reasoningParts) { | ||
| const decoded = Option.getOrUndefined(decodeReasoningPart(part)); | ||
| if (decoded) | ||
| content.push(decoded.text); | ||
| } | ||
| const itemText = joinReasoningText(summary) ?? joinReasoningText(content); | ||
| const events = []; | ||
| const metadata = reasoningMetadata(state, item); | ||
| const reasoningItem = state.reasoningItems[item.id]; | ||
| if (reasoningItem) { | ||
| if (!reasoningItem.open) | ||
| return [state, NO_EVENTS]; | ||
| const lifecycle = Object.entries(reasoningItem.summaryParts) | ||
| .filter((entry) => entry[1] === "active" || entry[1] === "can-conclude") | ||
| .reduce((lifecycle, entry) => Lifecycle.reasoningEnd(lifecycle, events, `${item.id}:${entry[0]}`, metadata), state.lifecycle); | ||
| const fragments = Object.entries(reasoningItem.summaryParts); | ||
| let lifecycle = state.lifecycle; | ||
| for (const [index, status] of fragments) { | ||
| if (status === "concluded") | ||
| continue; | ||
| // Do not repeat earlier summaries that were already emitted as separate fragments. | ||
| const finalText = fragments.length === 1 ? itemText : summary[Number(index)]; | ||
| lifecycle = Lifecycle.reasoningEnd(lifecycle, events, `${item.id}:${index}`, metadata, finalText || undefined); | ||
| } | ||
| return [ | ||
@@ -917,3 +957,7 @@ { | ||
| events.push(LLMEvent.reasoningStart({ id: item.id, providerMetadata: metadata })); | ||
| events.push(LLMEvent.reasoningEnd({ id: item.id, providerMetadata: metadata })); | ||
| events.push(LLMEvent.reasoningEnd({ | ||
| id: item.id, | ||
| providerMetadata: metadata, | ||
| text: itemText, | ||
| })); | ||
| return [ | ||
@@ -944,13 +988,16 @@ { | ||
| const onResponseFinish = Effect.fn("OpenResponses.onResponseFinish")(function* (state, event) { | ||
| const reconciled = event.type === "response.completed" | ||
| ? yield* Effect.reduce(event.response?.output ?? [], () => [state, NO_EVENTS], ([current, events], item) => { | ||
| let current = state; | ||
| const events = []; | ||
| if (event.type === "response.completed") { | ||
| for (const item of event.response?.output ?? []) { | ||
| const id = item.id ?? (item.type === "function_call" ? item.call_id : undefined); | ||
| if (id === undefined || | ||
| ((item.type !== "function_call" || !current.tools[id]) && | ||
| (item.type !== "reasoning" || !current.reasoningItems[id]?.open))) | ||
| return Effect.succeed([current, events]); | ||
| return onOutputItemDone(current, { type: "response.output_item.done", item }).pipe(Effect.map(([next, emitted]) => [next, [...events, ...emitted]])); | ||
| }) | ||
| : [state, NO_EVENTS]; | ||
| const current = reconciled[0]; | ||
| if (id === undefined) | ||
| continue; | ||
| if (item.type !== "function_call" || !current.tools[id]) | ||
| continue; | ||
| const [next, emitted] = yield* onOutputItemDone(current, item); | ||
| current = next; | ||
| events.push(...emitted); | ||
| } | ||
| } | ||
| // Some compatible providers omit output_item.done even after completing the response. | ||
@@ -960,3 +1007,3 @@ const pending = event.type === "response.completed" | ||
| : { tools: current.tools, events: NO_EVENTS }; | ||
| const events = [...reconciled[1], ...pending.events]; | ||
| events.push(...pending.events); | ||
| const hasFunctionCall = pending.events.some((event) => LLMEvent.is.toolCall(event) || LLMEvent.is.toolInputError(event)) || | ||
@@ -1070,3 +1117,3 @@ current.hasFunctionCall; | ||
| return ProviderShared.eventError(state.id, `${event.type} message is missing id`); | ||
| return onOutputItemDone(state, event); | ||
| return onOutputItemDone(state, event.item); | ||
| } | ||
@@ -1073,0 +1120,0 @@ if (event.type === "response.completed" || event.type === "response.incomplete") |
@@ -646,3 +646,8 @@ import { Effect, Schema } from "effect"; | ||
| default: | ||
| return "unknown"; | ||
| return yield* new AIError({ | ||
| reason: new UnknownProviderError({ | ||
| message: `Provider finish_reason: ${reason}`, | ||
| body: ProviderShared.encodeJson(event), | ||
| }), | ||
| }); | ||
| } | ||
@@ -850,7 +855,13 @@ }); | ||
| } | ||
| if (finishReason !== undefined && state.finishReason === undefined && Object.keys(pendingTools).length > 0) | ||
| const incompleteTools = finishReason?.normalized === "content-filter" || finishReason?.normalized === "length"; | ||
| if (finishReason !== undefined && | ||
| !incompleteTools && | ||
| state.finishReason === undefined && | ||
| Object.keys(pendingTools).length) | ||
| return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat tool call delta is missing id or name", ProviderShared.encodeJson(event)); | ||
| // Finalize accumulated tool inputs eagerly when finish_reason arrives so | ||
| // valid calls and malformed local calls settle independently. | ||
| const finished = finishReason !== undefined && state.finishReason === undefined && Object.keys(tools).length > 0 | ||
| // Filtering or truncation terminates the response without confirming pending tool calls. | ||
| const finished = finishReason !== undefined && | ||
| !incompleteTools && | ||
| state.finishReason === undefined && | ||
| Object.keys(tools).length > 0 | ||
| ? yield* ToolStream.finishAll(ADAPTER, tools) | ||
@@ -857,0 +868,0 @@ : undefined; |
@@ -58,2 +58,3 @@ import { Option, Schema } from "effect"; | ||
| const CONTENT_POLICY_TEXT = /content[-_\s]?policy|content_filter|safety/i; | ||
| const SERVER_ERROR_TEXT = /\b(?:try again|(?:please |you can )?retry (?:the |this |your )?request|try (?:the |this |your )?request again|(?:currently |temporarily )?at capacity|overloaded|temporarily unavailable|service[-_\s]?unavailable|(?:server|internal)[-_\s]?error|server (?:is )?busy|provider returned (?:an )?error|resource[-_\s]?exhausted|upstream (?:connect|connection|request)|request buffer limit while retrying upstream)\b/i; | ||
| // Classification records affirmative evidence about a failure. Deterministic | ||
@@ -98,2 +99,5 @@ // failures need positive identification (a 4xx status, quota/auth/policy | ||
| (input.status !== undefined && input.status >= 500) || | ||
| ((input.status === undefined || input.status < 400) && | ||
| !codes.some((code) => INVALID_REQUEST_CODES.has(code)) && | ||
| SERVER_ERROR_TEXT.test(text)) || | ||
| codes.some((code) => SERVER_CODES.has(code) || code.includes("exhausted") || code.includes("unavailable"))) | ||
@@ -100,0 +104,0 @@ return new ProviderInternalError({ |
+3
-3
| { | ||
| "$schema": "https://json.schemastore.org/package.json", | ||
| "version": "0.0.0-beta-18600", | ||
| "version": "0.0.0-beta-18684", | ||
| "name": "@opencode-ai/ai", | ||
@@ -33,3 +33,3 @@ "type": "module", | ||
| "@effect/platform-node": "4.0.0-rc.112", | ||
| "@opencode-ai/http-recorder": "0.0.0-beta-18600", | ||
| "@opencode-ai/http-recorder": "0.0.0-beta-18684", | ||
| "@tsconfig/bun": "1.0.9", | ||
@@ -43,3 +43,3 @@ "@types/bun": "1.3.13", | ||
| "@smithy/util-utf8": "4.2.2", | ||
| "@opencode-ai/schema": "0.0.0-beta-18600", | ||
| "@opencode-ai/schema": "0.0.0-beta-18684", | ||
| "aws4fetch": "1.0.20", | ||
@@ -46,0 +46,0 @@ "effect": "4.0.0-rc.112", |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1482704
0.21%34133
0.18%+ Added
- Removed