@jini-ai/agent-runtime 0.3.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +103 -0
- package/README.md +34 -17
- package/dist/acp-model-probe.d.ts +6 -4
- package/dist/acp-model-probe.js +3 -3
- package/dist/agent-protocol/acp/account-failure.d.ts +4 -2
- package/dist/agent-protocol/acp/account-failure.js +1 -1
- package/dist/agent-protocol/acp/json.d.ts +6 -6
- package/dist/agent-protocol/acp/json.js +3 -3
- package/dist/agent-protocol/acp/models.d.ts +10 -5
- package/dist/agent-protocol/acp/models.js +32 -31
- package/dist/agent-protocol/acp/session-params.d.ts +3 -1
- package/dist/agent-protocol/acp/session-params.js +2 -1
- package/dist/agent-protocol/acp/session.d.ts +28 -22
- package/dist/agent-protocol/acp/session.js +181 -147
- package/dist/agent-protocol/acp/text-suppression.d.ts +1 -1
- package/dist/agent-protocol/acp/text-suppression.js +1 -1
- package/dist/agent-protocol/acp/types.d.ts +3 -2
- package/dist/agent-protocol/acp/updates.d.ts +11 -11
- package/dist/agent-protocol/acp/updates.js +2 -2
- package/dist/agent-protocol/core/json-line-stream.d.ts +24 -8
- package/dist/agent-protocol/core/json-line-stream.js +8 -8
- package/dist/agent-protocol/pi-rpc/events.d.ts +5 -1
- package/dist/agent-protocol/pi-rpc/events.js +36 -36
- package/dist/agent-protocol/pi-rpc/internal.d.ts +4 -1
- package/dist/agent-protocol/pi-rpc/models.d.ts +3 -1
- package/dist/agent-protocol/pi-rpc/models.js +1 -1
- package/dist/agent-protocol/pi-rpc/session.d.ts +1 -1
- package/dist/agent-protocol/pi-rpc/session.js +33 -32
- package/dist/amr-model-cache.d.ts +12 -4
- package/dist/amr-model-cache.js +17 -21
- package/dist/amr-profile-resolver.d.ts +3 -1
- package/dist/anthropic-live-models.js +0 -0
- package/dist/artifact-taxonomy.d.ts +6 -2
- package/dist/auth.d.ts +52 -15
- package/dist/auth.js +36 -36
- package/dist/claude-code-models.js +2 -2
- package/dist/claude-stream.d.ts +22 -3
- package/dist/claude-stream.js +24 -8
- package/dist/copilot-stream.d.ts +7 -3
- package/dist/copilot-stream.js +3 -3
- package/dist/defs/aider.d.ts +8 -1
- package/dist/defs/aider.js +2 -2
- package/dist/defs/amp.d.ts +8 -1
- package/dist/defs/amp.js +2 -2
- package/dist/defs/amr.d.ts +22 -6
- package/dist/defs/amr.js +23 -23
- package/dist/defs/antigravity.d.ts +15 -4
- package/dist/defs/antigravity.js +9 -8
- package/dist/defs/auggie.d.ts +4 -1
- package/dist/defs/auggie.js +1 -1
- package/dist/defs/claude.d.ts +16 -2
- package/dist/defs/claude.js +22 -4
- package/dist/defs/cline.d.ts +4 -1
- package/dist/defs/cline.js +1 -1
- package/dist/defs/codebuddy.d.ts +8 -1
- package/dist/defs/codebuddy.js +2 -2
- package/dist/defs/codex.d.ts +18 -4
- package/dist/defs/codex.js +7 -7
- package/dist/defs/copilot.d.ts +8 -1
- package/dist/defs/copilot.js +2 -2
- package/dist/defs/crush.d.ts +8 -1
- package/dist/defs/crush.js +1 -1
- package/dist/defs/cursor-agent.d.ts +14 -3
- package/dist/defs/cursor-agent.js +5 -5
- package/dist/defs/deepseek.d.ts +8 -1
- package/dist/defs/deepseek.js +2 -2
- package/dist/defs/devin.d.ts +12 -2
- package/dist/defs/devin.js +3 -3
- package/dist/defs/droid.d.ts +8 -1
- package/dist/defs/droid.js +1 -1
- package/dist/defs/gemini.d.ts +8 -1
- package/dist/defs/gemini.js +1 -1
- package/dist/defs/goose.d.ts +4 -1
- package/dist/defs/goose.js +1 -1
- package/dist/defs/grok-build.d.ts +11 -2
- package/dist/defs/grok-build.js +3 -3
- package/dist/defs/hermes.d.ts +4 -1
- package/dist/defs/hermes.js +2 -2
- package/dist/defs/kilo.d.ts +4 -1
- package/dist/defs/kilo.js +2 -2
- package/dist/defs/kimi.d.ts +4 -1
- package/dist/defs/kimi.js +2 -2
- package/dist/defs/kiro.d.ts +4 -1
- package/dist/defs/kiro.js +2 -2
- package/dist/defs/mimo.d.ts +8 -1
- package/dist/defs/mimo.js +2 -2
- package/dist/defs/opencode.d.ts +9 -2
- package/dist/defs/opencode.js +2 -2
- package/dist/defs/pi.d.ts +12 -2
- package/dist/defs/pi.js +9 -9
- package/dist/defs/qoder.d.ts +8 -1
- package/dist/defs/qoder.js +2 -2
- package/dist/defs/qwen.d.ts +8 -1
- package/dist/defs/qwen.js +2 -2
- package/dist/defs/reasonix.d.ts +4 -1
- package/dist/defs/reasonix.js +3 -3
- package/dist/defs/shared.d.ts +4 -2
- package/dist/defs/shared.js +2 -2
- package/dist/defs/trae-cli.d.ts +12 -2
- package/dist/defs/trae-cli.js +3 -3
- package/dist/defs/vibe.d.ts +4 -1
- package/dist/defs/vibe.js +2 -2
- package/dist/detection.d.ts +38 -3
- package/dist/detection.js +77 -37
- package/dist/diagnostics.d.ts +14 -3
- package/dist/diagnostics.js +5 -5
- package/dist/env.d.ts +16 -4
- package/dist/env.js +7 -7
- package/dist/executables.d.ts +24 -8
- package/dist/executables.js +13 -13
- package/dist/index.d.ts +228 -4
- package/dist/index.js +230 -6
- package/dist/invocation.d.ts +6 -1
- package/dist/invocation.js +2 -2
- package/dist/json-event-stream.d.ts +7 -2
- package/dist/json-event-stream.js +3 -3
- package/dist/launch.d.ts +12 -2
- package/dist/launch.js +3 -3
- package/dist/log-file.d.ts +4 -1
- package/dist/log-file.js +2 -2
- package/dist/mcp.d.ts +6 -3
- package/dist/mcp.js +1 -1
- package/dist/metadata.d.ts +7 -3
- package/dist/metadata.js +1 -1
- package/dist/mmd-routes.d.ts +22 -6
- package/dist/mmd-routes.js +14 -14
- package/dist/model-catalog-cache.d.ts +50 -0
- package/dist/model-catalog-cache.js +61 -0
- package/dist/model-loading-state.d.ts +10 -0
- package/dist/model-loading-state.js +23 -0
- package/dist/model-registry.d.ts +28 -7
- package/dist/model-registry.js +11 -8
- package/dist/models.d.ts +31 -6
- package/dist/models.js +6 -6
- package/dist/opencode-log.d.ts +12 -4
- package/dist/opencode-log.js +8 -8
- package/dist/paths.d.ts +6 -2
- package/dist/paths.js +3 -3
- package/dist/pi-models.d.ts +3 -1
- package/dist/pi-models.js +2 -2
- package/dist/prompt-budget.d.ts +17 -4
- package/dist/prompt-budget.js +3 -3
- package/dist/prompt-file.d.ts +5 -1
- package/dist/prompt-file.js +1 -1
- package/dist/providers/aihubmix.d.ts +34 -9
- package/dist/providers/aihubmix.js +26 -26
- package/dist/providers/anthropic-messages.d.ts +77 -20
- package/dist/providers/anthropic-messages.js +53 -50
- package/dist/providers/azure-chat.d.ts +21 -7
- package/dist/providers/azure-chat.js +18 -16
- package/dist/providers/connection-guard.d.ts +45 -31
- package/dist/providers/connection-guard.js +40 -143
- package/dist/providers/connection-test.d.ts +1 -1
- package/dist/providers/connection-test.js +13 -30
- package/dist/providers/elevenlabs.d.ts +7 -2
- package/dist/providers/elevenlabs.js +4 -4
- package/dist/providers/google-messages.d.ts +44 -10
- package/dist/providers/google-messages.js +36 -34
- package/dist/providers/google-schema.d.ts +26 -0
- package/dist/providers/google-schema.js +253 -0
- package/dist/providers/google.d.ts +21 -6
- package/dist/providers/google.js +10 -10
- package/dist/providers/index.d.ts +3 -2
- package/dist/providers/index.js +3 -2
- package/dist/providers/model-catalog.d.ts +1 -1
- package/dist/providers/model-catalog.js +14 -27
- package/dist/providers/oauth-callback-server.d.ts +1 -1
- package/dist/providers/oauth-callback-server.js +3 -2
- package/dist/providers/oauth-credentials.d.ts +7 -1
- package/dist/providers/oauth-credentials.js +5 -5
- package/dist/providers/oauth-provider.d.ts +2 -2
- package/dist/providers/oauth-provider.js +20 -25
- package/dist/providers/oauth-tokens.d.ts +26 -6
- package/dist/providers/oauth-tokens.js +9 -9
- package/dist/providers/ollama-chat.d.ts +66 -31
- package/dist/providers/ollama-chat.js +64 -61
- package/dist/providers/openai-chat.d.ts +77 -22
- package/dist/providers/openai-chat.js +71 -67
- package/dist/providers/pkce.d.ts +33 -20
- package/dist/providers/pkce.js +75 -146
- package/dist/providers/sse-decode.d.ts +3 -1
- package/dist/providers/sse-decode.js +1 -1
- package/dist/providers/token-params.d.ts +16 -5
- package/dist/providers/token-params.js +6 -6
- package/dist/providers/tool-turn-types.d.ts +89 -0
- package/dist/providers/tool-turn-types.js +2 -0
- package/dist/providers/tool-turn.d.ts +10 -0
- package/dist/providers/tool-turn.js +252 -0
- package/dist/qoder-stream.d.ts +7 -3
- package/dist/qoder-stream.js +3 -3
- package/dist/registry.d.ts +6 -2
- package/dist/registry.js +4 -4
- package/dist/resolution.d.ts +5 -1
- package/dist/resolution.js +3 -3
- package/dist/role-marker-guard.d.ts +9 -5
- package/dist/role-marker-guard.js +5 -5
- package/dist/terminal-launch.d.ts +6 -1
- package/dist/terminal-launch.js +2 -2
- package/dist/types.d.ts +37 -6
- package/package.json +43 -4
|
@@ -178,10 +178,17 @@ export interface OpenAiTurnResult {
|
|
|
178
178
|
}
|
|
179
179
|
/** Matches OD's own default (`apps/daemon/src/routes/chat.ts`'s openai/azure handlers both fall back to this when the caller doesn't supply one) — a real, explicit bound is always sent, never an unbounded request. */
|
|
180
180
|
export declare const DEFAULT_OPENAI_MAX_TOKENS = 8192;
|
|
181
|
-
export declare function openAiRequestUrl(baseUrl
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
export declare function
|
|
181
|
+
export declare function openAiRequestUrl({ baseUrl }: {
|
|
182
|
+
baseUrl: string | undefined;
|
|
183
|
+
}): string;
|
|
184
|
+
export declare function openAiHeaders(requiredArgs: Pick<OpenAiTurnOptions, "apiKey" | "model" | "messages" | "onEvent">, optionalArgs?: Omit<OpenAiTurnOptions, "apiKey" | "model" | "messages" | "onEvent">): Record<string, string>;
|
|
185
|
+
export declare function openAiRequestBody({ options, messages }: {
|
|
186
|
+
options: OpenAiTurnOptions;
|
|
187
|
+
messages: readonly OpenAiMessageParam[];
|
|
188
|
+
}): Record<string, unknown>;
|
|
189
|
+
export declare function extractOpenAiErrorDetail({ rawText }: {
|
|
190
|
+
rawText: string;
|
|
191
|
+
}): string;
|
|
185
192
|
export interface PendingToolCall {
|
|
186
193
|
id: string;
|
|
187
194
|
name: string;
|
|
@@ -233,24 +240,48 @@ export interface OpenAiStreamState {
|
|
|
233
240
|
usage: Record<string, unknown> | null;
|
|
234
241
|
}
|
|
235
242
|
/** Parses one SSE frame's data as a JSON object, or `null` for a malformed/empty keep-alive frame or a non-object payload — both are tolerated by the caller as "nothing to do this frame". */
|
|
236
|
-
export declare function parseOpenAiSseData(raw
|
|
237
|
-
|
|
243
|
+
export declare function parseOpenAiSseData({ raw }: {
|
|
244
|
+
raw: string;
|
|
245
|
+
}): Record<string, unknown> | null;
|
|
246
|
+
export declare function applyOpenAiStreamUsage({ state, data, onEvent }: {
|
|
247
|
+
state: OpenAiStreamState;
|
|
248
|
+
data: Record<string, unknown>;
|
|
249
|
+
onEvent: (event: OpenAiTurnEvent) => void;
|
|
250
|
+
}): void;
|
|
238
251
|
/** The chunk's first (and, for Chat Completions, only) choice — `null` when the chunk carries no choice at all (e.g. a usage-only trailer chunk). */
|
|
239
|
-
export declare function firstOpenAiChoice(data
|
|
252
|
+
export declare function firstOpenAiChoice({ data }: {
|
|
253
|
+
data: Record<string, unknown>;
|
|
254
|
+
}): Record<string, unknown> | null;
|
|
240
255
|
/**
|
|
241
256
|
* Feeds one text delta through the role-marker guard and emits the safe portion. Returns
|
|
242
257
|
* `'break'` once the guard flags contamination (the caller ends the turn immediately), otherwise
|
|
243
258
|
* `'continue'` — same contract as `anthropic-messages.ts#handleAnthropicTextDelta`.
|
|
244
259
|
*/
|
|
245
|
-
export declare function handleOpenAiTextContentDelta(state
|
|
260
|
+
export declare function handleOpenAiTextContentDelta({ state, content, onEvent }: {
|
|
261
|
+
state: OpenAiStreamState;
|
|
262
|
+
content: string;
|
|
263
|
+
onEvent: (event: OpenAiTurnEvent) => void;
|
|
264
|
+
}): 'continue' | 'break';
|
|
246
265
|
/** Builds a fresh `PendingToolCall` from the streaming chunk that first mentions a given tool-call index — OpenAI sends `id`/`function.name` once, on that first chunk, then dribbles `function.arguments` in across subsequent chunks (accumulated separately by the caller). */
|
|
247
|
-
export declare function newPendingOpenAiToolCall(rawCall
|
|
266
|
+
export declare function newPendingOpenAiToolCall({ rawCall, index }: {
|
|
267
|
+
rawCall: Record<string, unknown>;
|
|
268
|
+
index: number;
|
|
269
|
+
}): PendingToolCall;
|
|
248
270
|
/** Accumulates one `delta.tool_calls[]` entry from a streaming chunk into its running `PendingToolCall`. */
|
|
249
|
-
export declare function accumulateOpenAiToolCallDelta(state
|
|
271
|
+
export declare function accumulateOpenAiToolCallDelta({ state, rawCall }: {
|
|
272
|
+
state: OpenAiStreamState;
|
|
273
|
+
rawCall: unknown;
|
|
274
|
+
}): void;
|
|
250
275
|
/** Reduces one chunk's `choices[0]` — `finish_reason`, text content, and tool-call deltas — into `state`. Returns `'break'` when the text-delta guard detects contamination. */
|
|
251
|
-
export declare function handleOpenAiChoiceDelta(state
|
|
276
|
+
export declare function handleOpenAiChoiceDelta({ state, choice, onEvent }: {
|
|
277
|
+
state: OpenAiStreamState;
|
|
278
|
+
choice: Record<string, unknown>;
|
|
279
|
+
onEvent: (event: OpenAiTurnEvent) => void;
|
|
280
|
+
}): 'continue' | 'break';
|
|
252
281
|
/** Parses one accumulated tool call's `argsJson`, falling back to `{}` for empty or malformed JSON — mirrors Anthropic's identical fallback for `input_json_delta` accumulation in `anthropic-messages.ts`. */
|
|
253
|
-
export declare function resolveOpenAiToolCalls(pending
|
|
282
|
+
export declare function resolveOpenAiToolCalls({ pending }: {
|
|
283
|
+
pending: ReadonlyMap<number, PendingToolCall>;
|
|
284
|
+
}): OpenAiToolCall[];
|
|
254
285
|
export type OpenAiFrameResult = 'continue' | 'done' | 'contaminated';
|
|
255
286
|
/**
|
|
256
287
|
* Reduces one decoded SSE frame into `state`, reporting what the caller's loop should do next:
|
|
@@ -259,7 +290,11 @@ export type OpenAiFrameResult = 'continue' | 'done' | 'contaminated';
|
|
|
259
290
|
* turns what would be four sequential, nesting-penalized `if`s into a single function call plus
|
|
260
291
|
* one outcome check — see `runOpenAiCompatibleRequest`'s loop below.
|
|
261
292
|
*/
|
|
262
|
-
export declare function processOpenAiStreamFrame(state
|
|
293
|
+
export declare function processOpenAiStreamFrame({ state, frame, onEvent }: {
|
|
294
|
+
state: OpenAiStreamState;
|
|
295
|
+
frame: DecodedSseEvent;
|
|
296
|
+
onEvent: (event: OpenAiTurnEvent) => void;
|
|
297
|
+
}): OpenAiFrameResult;
|
|
263
298
|
/**
|
|
264
299
|
* Runs exactly one OpenAI-compatible (Chat Completions JSON wire format)
|
|
265
300
|
* streaming HTTP request and reduces its SSE events into a single outcome.
|
|
@@ -275,7 +310,7 @@ export declare function processOpenAiStreamFrame(state: OpenAiStreamState, frame
|
|
|
275
310
|
* `anthropic-messages.ts#runSingleAnthropicRequest`'s `emitEnd` contract —
|
|
276
311
|
* see that function's doc.
|
|
277
312
|
*/
|
|
278
|
-
export declare function runOpenAiCompatibleRequest(
|
|
313
|
+
export declare function runOpenAiCompatibleRequest(requiredArgs: Pick<OpenAiCompatibleRequestInit, "url" | "headers" | "body" | "redactSecretsList" | "guardMessageId" | "providerLabel" | "onEvent" | "emitEnd" | "hasEnded">, optionalArgs?: Omit<OpenAiCompatibleRequestInit, "url" | "headers" | "body" | "redactSecretsList" | "guardMessageId" | "providerLabel" | "onEvent" | "emitEnd" | "hasEnded">): Promise<OpenAiCompatibleRequestOutcome>;
|
|
279
314
|
/**
|
|
280
315
|
* Validates one content part against OpenAI's real format/size constraints. Returns the violation
|
|
281
316
|
* reason, or `null` when the part is legal to send.
|
|
@@ -288,7 +323,9 @@ export declare function runOpenAiCompatibleRequest(init: OpenAiCompatibleRequest
|
|
|
288
323
|
*
|
|
289
324
|
* @complexity O(1) — reads `data.length`, never decodes or parses the base64 payload.
|
|
290
325
|
*/
|
|
291
|
-
export declare function invalidOpenAiContentPartReason(part
|
|
326
|
+
export declare function invalidOpenAiContentPartReason({ part }: {
|
|
327
|
+
part: OpenAiContentPart;
|
|
328
|
+
}): string | null;
|
|
292
329
|
export interface SanitizedOpenAiToolResult {
|
|
293
330
|
readonly content: string | readonly OpenAiContentPart[];
|
|
294
331
|
readonly isError: boolean;
|
|
@@ -306,7 +343,9 @@ export interface SanitizedOpenAiToolResult {
|
|
|
306
343
|
*
|
|
307
344
|
* @complexity O(n) in the number of content parts; O(1) per part (see `invalidOpenAiContentPartReason`).
|
|
308
345
|
*/
|
|
309
|
-
export declare function sanitizeOpenAiToolResult(result
|
|
346
|
+
export declare function sanitizeOpenAiToolResult({ result }: {
|
|
347
|
+
result: OpenAiToolResult;
|
|
348
|
+
}): SanitizedOpenAiToolResult;
|
|
310
349
|
export interface SplitOpenAiToolResultContent {
|
|
311
350
|
readonly toolMessageContent: string | readonly OpenAiTextPart[];
|
|
312
351
|
readonly imageParts: readonly OpenAiImageUrlPart[];
|
|
@@ -323,13 +362,19 @@ export interface SplitOpenAiToolResultContent {
|
|
|
323
362
|
*
|
|
324
363
|
* @complexity O(n) in the number of content parts.
|
|
325
364
|
*/
|
|
326
|
-
export declare function splitOpenAiToolResultContent(content
|
|
365
|
+
export declare function splitOpenAiToolResultContent({ content }: {
|
|
366
|
+
content: string | readonly OpenAiContentPart[];
|
|
367
|
+
}): SplitOpenAiToolResultContent;
|
|
327
368
|
/**
|
|
328
369
|
* Decides why the tool loop should stop after one request, or returns `null` when it should
|
|
329
370
|
* instead proceed to execute the pending tool calls — mirrors
|
|
330
371
|
* `anthropic-messages.ts#anthropicLoopExitReason`'s pure decision/effect split.
|
|
331
372
|
*/
|
|
332
|
-
export declare function openAiLoopExitReason(outcome
|
|
373
|
+
export declare function openAiLoopExitReason({ outcome, toolTurns, maxToolTurns }: {
|
|
374
|
+
outcome: OpenAiCompatibleRequestOutcome;
|
|
375
|
+
toolTurns: number;
|
|
376
|
+
maxToolTurns: number;
|
|
377
|
+
}): OpenAiTurnEndReason | null;
|
|
333
378
|
export interface OpenAiToolExecutionOutcome {
|
|
334
379
|
readonly toolResultMessages: OpenAiMessageParam[];
|
|
335
380
|
readonly followUpParts: OpenAiContentPart[];
|
|
@@ -340,16 +385,26 @@ export interface OpenAiToolExecutionOutcome {
|
|
|
340
385
|
* module doc's "Tool messages cannot carry an image" section for why the split exists and why the
|
|
341
386
|
* follow-up is assembled once per batch rather than per call.
|
|
342
387
|
*/
|
|
343
|
-
export declare function executeOpenAiToolCalls(executeTool
|
|
388
|
+
export declare function executeOpenAiToolCalls({ executeTool, calls, onEvent }: {
|
|
389
|
+
executeTool: OpenAiToolExecutor;
|
|
390
|
+
calls: readonly OpenAiToolCall[];
|
|
391
|
+
onEvent: (event: OpenAiTurnEvent) => void;
|
|
392
|
+
}): Promise<OpenAiToolExecutionOutcome>;
|
|
344
393
|
/** Builds the assistant turn that records the model's pending tool calls in `messages` history — `content` falls back to `null` (never `''`) per the wire schema. */
|
|
345
|
-
export declare function buildOpenAiAssistantToolCallMessage(text
|
|
394
|
+
export declare function buildOpenAiAssistantToolCallMessage({ text, toolCalls }: {
|
|
395
|
+
text: string;
|
|
396
|
+
toolCalls: readonly OpenAiToolCallParam[];
|
|
397
|
+
}): OpenAiMessageParam;
|
|
346
398
|
/** Appends the batch's `tool` messages plus, when present, the single labeled-image follow-up message — see module doc's "Tool messages cannot carry an image" section for why the follow-up is at most one message per batch. */
|
|
347
|
-
export declare function buildOpenAiToolExchangeMessages(toolResultMessages
|
|
399
|
+
export declare function buildOpenAiToolExchangeMessages({ toolResultMessages, followUpParts }: {
|
|
400
|
+
toolResultMessages: readonly OpenAiMessageParam[];
|
|
401
|
+
followUpParts: readonly OpenAiContentPart[];
|
|
402
|
+
}): OpenAiMessageParam[];
|
|
348
403
|
/**
|
|
349
404
|
* Runs a full OpenAI Chat Completions turn, including the tool-execution
|
|
350
405
|
* loop when `options.executeTool` is supplied and the model requests a
|
|
351
406
|
* function call. See `anthropic-messages.ts#runAnthropicToolTurn`'s doc for
|
|
352
407
|
* the shared event-stream/`ended`-flag contract this mirrors exactly.
|
|
353
408
|
*/
|
|
354
|
-
export declare function runOpenAiToolTurn(
|
|
409
|
+
export declare function runOpenAiToolTurn(requiredArgs: Pick<OpenAiTurnOptions, "apiKey" | "model" | "messages" | "onEvent">, optionalArgs?: Omit<OpenAiTurnOptions, "apiKey" | "model" | "messages" | "onEvent">): Promise<OpenAiTurnResult>;
|
|
355
410
|
//# sourceMappingURL=openai-chat.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { redactSecrets } from '@jini-ai/core';
|
|
1
2
|
/**
|
|
2
3
|
* @module providers/openai-chat
|
|
3
4
|
*
|
|
@@ -67,7 +68,7 @@
|
|
|
67
68
|
* reasoning.
|
|
68
69
|
*/
|
|
69
70
|
import { createRoleMarkerGuard } from '../role-marker-guard.js';
|
|
70
|
-
import { defaultDnsLookup, pinnedFetch,
|
|
71
|
+
import { defaultDnsLookup, pinnedFetch, validateBaseUrlResolved } from './connection-guard.js';
|
|
71
72
|
import { decodeSseStream } from './sse-decode.js';
|
|
72
73
|
import { buildOpenAIChatTokenParam } from './token-params.js';
|
|
73
74
|
import { createTurnEndGuard } from './turn-end-guard.js';
|
|
@@ -79,30 +80,31 @@ export const DEFAULT_OPENAI_MAX_TOKENS = 8192;
|
|
|
79
80
|
function isRecord(value) {
|
|
80
81
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
81
82
|
}
|
|
82
|
-
export function openAiRequestUrl(baseUrl) {
|
|
83
|
+
export function openAiRequestUrl({ baseUrl }) {
|
|
83
84
|
const base = (baseUrl ?? DEFAULT_OPENAI_BASE_URL).replace(/\/+$/, '');
|
|
84
85
|
return /\/v\d+(\/|$)/.test(base) ? `${base}/chat/completions` : `${base}/v1/chat/completions`;
|
|
85
86
|
}
|
|
86
|
-
export function openAiHeaders(
|
|
87
|
+
export function openAiHeaders(requiredArgs, optionalArgs = {}) {
|
|
88
|
+
const options = { ...optionalArgs, ...requiredArgs };
|
|
87
89
|
return {
|
|
88
90
|
'content-type': 'application/json',
|
|
89
91
|
authorization: `Bearer ${options.apiKey}`,
|
|
90
92
|
...(options.extraHeaders ?? {}),
|
|
91
93
|
};
|
|
92
94
|
}
|
|
93
|
-
export function openAiRequestBody(options, messages) {
|
|
95
|
+
export function openAiRequestBody({ options, messages }) {
|
|
94
96
|
const effectiveMaxTokens = typeof options.maxTokens === 'number' && options.maxTokens > 0 ? options.maxTokens : DEFAULT_OPENAI_MAX_TOKENS;
|
|
95
97
|
return {
|
|
96
98
|
model: options.model,
|
|
97
99
|
stream: true,
|
|
98
100
|
stream_options: { include_usage: true },
|
|
99
101
|
messages,
|
|
100
|
-
...buildOpenAIChatTokenParam(options.model, effectiveMaxTokens),
|
|
102
|
+
...buildOpenAIChatTokenParam({ model: options.model, maxTokens: effectiveMaxTokens }),
|
|
101
103
|
...(options.temperature !== undefined ? { temperature: options.temperature } : {}),
|
|
102
104
|
...(options.tools && options.tools.length > 0 ? { tools: options.tools } : {}),
|
|
103
105
|
};
|
|
104
106
|
}
|
|
105
|
-
export function extractOpenAiErrorDetail(rawText) {
|
|
107
|
+
export function extractOpenAiErrorDetail({ rawText }) {
|
|
106
108
|
try {
|
|
107
109
|
const parsed = JSON.parse(rawText);
|
|
108
110
|
if (isRecord(parsed) && isRecord(parsed.error) && typeof parsed.error.message === 'string') {
|
|
@@ -115,7 +117,7 @@ export function extractOpenAiErrorDetail(rawText) {
|
|
|
115
117
|
return rawText.trim().slice(0, 500);
|
|
116
118
|
}
|
|
117
119
|
/** Parses one SSE frame's data as a JSON object, or `null` for a malformed/empty keep-alive frame or a non-object payload — both are tolerated by the caller as "nothing to do this frame". */
|
|
118
|
-
export function parseOpenAiSseData(raw) {
|
|
120
|
+
export function parseOpenAiSseData({ raw }) {
|
|
119
121
|
let data;
|
|
120
122
|
try {
|
|
121
123
|
data = JSON.parse(raw);
|
|
@@ -125,14 +127,14 @@ export function parseOpenAiSseData(raw) {
|
|
|
125
127
|
}
|
|
126
128
|
return isRecord(data) ? data : null;
|
|
127
129
|
}
|
|
128
|
-
export function applyOpenAiStreamUsage(state, data, onEvent) {
|
|
130
|
+
export function applyOpenAiStreamUsage({ state, data, onEvent }) {
|
|
129
131
|
if (!isRecord(data.usage))
|
|
130
132
|
return;
|
|
131
133
|
state.usage = data.usage;
|
|
132
134
|
onEvent({ type: 'usage', usage: data.usage });
|
|
133
135
|
}
|
|
134
136
|
/** The chunk's first (and, for Chat Completions, only) choice — `null` when the chunk carries no choice at all (e.g. a usage-only trailer chunk). */
|
|
135
|
-
export function firstOpenAiChoice(data) {
|
|
137
|
+
export function firstOpenAiChoice({ data }) {
|
|
136
138
|
const choices = Array.isArray(data.choices) ? data.choices : [];
|
|
137
139
|
const choice = choices[0];
|
|
138
140
|
return isRecord(choice) ? choice : null;
|
|
@@ -142,8 +144,8 @@ export function firstOpenAiChoice(data) {
|
|
|
142
144
|
* `'break'` once the guard flags contamination (the caller ends the turn immediately), otherwise
|
|
143
145
|
* `'continue'` — same contract as `anthropic-messages.ts#handleAnthropicTextDelta`.
|
|
144
146
|
*/
|
|
145
|
-
export function handleOpenAiTextContentDelta(state, content, onEvent) {
|
|
146
|
-
const safe = state.guard.feedText(content);
|
|
147
|
+
export function handleOpenAiTextContentDelta({ state, content, onEvent }) {
|
|
148
|
+
const safe = state.guard.feedText({ text: content });
|
|
147
149
|
if (safe.length > 0) {
|
|
148
150
|
state.fullText += safe;
|
|
149
151
|
onEvent({ type: 'text_delta', delta: safe });
|
|
@@ -156,19 +158,19 @@ export function handleOpenAiTextContentDelta(state, content, onEvent) {
|
|
|
156
158
|
return 'break';
|
|
157
159
|
}
|
|
158
160
|
/** Builds a fresh `PendingToolCall` from the streaming chunk that first mentions a given tool-call index — OpenAI sends `id`/`function.name` once, on that first chunk, then dribbles `function.arguments` in across subsequent chunks (accumulated separately by the caller). */
|
|
159
|
-
export function newPendingOpenAiToolCall(rawCall, index) {
|
|
161
|
+
export function newPendingOpenAiToolCall({ rawCall, index }) {
|
|
160
162
|
const id = typeof rawCall.id === 'string' ? rawCall.id : `call_${index}`;
|
|
161
163
|
const fn = isRecord(rawCall.function) ? rawCall.function : {};
|
|
162
164
|
const name = typeof fn.name === 'string' ? fn.name : '';
|
|
163
165
|
return { id, name, argsJson: '' };
|
|
164
166
|
}
|
|
165
167
|
/** Accumulates one `delta.tool_calls[]` entry from a streaming chunk into its running `PendingToolCall`. */
|
|
166
|
-
export function accumulateOpenAiToolCallDelta(state, rawCall) {
|
|
168
|
+
export function accumulateOpenAiToolCallDelta({ state, rawCall }) {
|
|
167
169
|
if (!isRecord(rawCall) || typeof rawCall.index !== 'number')
|
|
168
170
|
return;
|
|
169
171
|
let pending = state.toolCalls.get(rawCall.index);
|
|
170
172
|
if (!pending) {
|
|
171
|
-
pending = newPendingOpenAiToolCall(rawCall, rawCall.index);
|
|
173
|
+
pending = newPendingOpenAiToolCall({ rawCall: rawCall, index: rawCall.index });
|
|
172
174
|
state.toolCalls.set(rawCall.index, pending);
|
|
173
175
|
}
|
|
174
176
|
const fn = isRecord(rawCall.function) ? rawCall.function : null;
|
|
@@ -177,7 +179,7 @@ export function accumulateOpenAiToolCallDelta(state, rawCall) {
|
|
|
177
179
|
}
|
|
178
180
|
}
|
|
179
181
|
/** Reduces one chunk's `choices[0]` — `finish_reason`, text content, and tool-call deltas — into `state`. Returns `'break'` when the text-delta guard detects contamination. */
|
|
180
|
-
export function handleOpenAiChoiceDelta(state, choice, onEvent) {
|
|
182
|
+
export function handleOpenAiChoiceDelta({ state, choice, onEvent }) {
|
|
181
183
|
if (typeof choice.finish_reason === 'string') {
|
|
182
184
|
state.finishReason = choice.finish_reason;
|
|
183
185
|
}
|
|
@@ -185,18 +187,18 @@ export function handleOpenAiChoiceDelta(state, choice, onEvent) {
|
|
|
185
187
|
if (!delta)
|
|
186
188
|
return 'continue';
|
|
187
189
|
if (typeof delta.content === 'string' && delta.content.length > 0) {
|
|
188
|
-
if (handleOpenAiTextContentDelta(state, delta.content, onEvent) === 'break')
|
|
190
|
+
if (handleOpenAiTextContentDelta({ state: state, content: delta.content, onEvent: onEvent }) === 'break')
|
|
189
191
|
return 'break';
|
|
190
192
|
}
|
|
191
193
|
if (Array.isArray(delta.tool_calls)) {
|
|
192
194
|
for (const rawCall of delta.tool_calls) {
|
|
193
|
-
accumulateOpenAiToolCallDelta(state, rawCall);
|
|
195
|
+
accumulateOpenAiToolCallDelta({ state: state, rawCall: rawCall });
|
|
194
196
|
}
|
|
195
197
|
}
|
|
196
198
|
return 'continue';
|
|
197
199
|
}
|
|
198
200
|
/** Parses one accumulated tool call's `argsJson`, falling back to `{}` for empty or malformed JSON — mirrors Anthropic's identical fallback for `input_json_delta` accumulation in `anthropic-messages.ts`. */
|
|
199
|
-
export function resolveOpenAiToolCalls(pending) {
|
|
201
|
+
export function resolveOpenAiToolCalls({ pending }) {
|
|
200
202
|
return Array.from(pending.values()).map((call) => {
|
|
201
203
|
let input = {};
|
|
202
204
|
if (call.argsJson.trim()) {
|
|
@@ -226,26 +228,26 @@ async function requestOpenAiCompatibleStream(init) {
|
|
|
226
228
|
const { onEvent, emitEnd } = init;
|
|
227
229
|
let response;
|
|
228
230
|
try {
|
|
229
|
-
response = await (init.fetchImpl ?? pinnedFetch)(init.url, {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
231
|
+
response = await (init.fetchImpl ?? pinnedFetch)({ url: init.url, init: {
|
|
232
|
+
method: 'POST',
|
|
233
|
+
headers: init.headers,
|
|
234
|
+
body: JSON.stringify(init.body),
|
|
235
|
+
// The caller's SSRF check (`validateBaseUrlResolved`) only ever sees the
|
|
236
|
+
// ORIGINAL url. Following a redirect would let a public, guard-passing
|
|
237
|
+
// endpoint hand back a `302 -> http://169.254.169.254/...` and reach the
|
|
238
|
+
// address the guard exists to refuse — with the provider auth headers
|
|
239
|
+
// still attached. `model-catalog.ts`'s own fetch already refuses
|
|
240
|
+
// redirects for the same reason; a redirecting chat-completions endpoint
|
|
241
|
+
// is not a thing any supported provider does. `pinnedFetch` never
|
|
242
|
+
// follows one regardless (see its doc); `redirect: 'error'` here is
|
|
243
|
+
// self-documentation, not the mechanism.
|
|
244
|
+
redirect: 'error',
|
|
245
|
+
...(init.signal ? { signal: init.signal } : {}),
|
|
246
|
+
}, pinnedAddress: init.pinnedAddress });
|
|
245
247
|
}
|
|
246
248
|
catch (error) {
|
|
247
249
|
const message = error instanceof Error ? error.message : String(error);
|
|
248
|
-
onEvent({ type: 'error', message: redactSecrets(message, init.redactSecretsList) });
|
|
250
|
+
onEvent({ type: 'error', message: redactSecrets({ input: message }, { exactSecrets: init.redactSecretsList }) });
|
|
249
251
|
emitEnd('error');
|
|
250
252
|
return { type: 'ended' };
|
|
251
253
|
}
|
|
@@ -258,7 +260,7 @@ async function requestOpenAiCompatibleStream(init) {
|
|
|
258
260
|
}
|
|
259
261
|
onEvent({
|
|
260
262
|
type: 'error',
|
|
261
|
-
message: redactSecrets(extractOpenAiErrorDetail(rawText), init.redactSecretsList),
|
|
263
|
+
message: redactSecrets({ input: extractOpenAiErrorDetail({ rawText: rawText }) }, { exactSecrets: init.redactSecretsList }),
|
|
262
264
|
code: String(response.status),
|
|
263
265
|
});
|
|
264
266
|
emitEnd('error');
|
|
@@ -278,17 +280,17 @@ async function requestOpenAiCompatibleStream(init) {
|
|
|
278
280
|
* turns what would be four sequential, nesting-penalized `if`s into a single function call plus
|
|
279
281
|
* one outcome check — see `runOpenAiCompatibleRequest`'s loop below.
|
|
280
282
|
*/
|
|
281
|
-
export function processOpenAiStreamFrame(state, frame, onEvent) {
|
|
283
|
+
export function processOpenAiStreamFrame({ state, frame, onEvent }) {
|
|
282
284
|
if (frame.data === DONE_SENTINEL)
|
|
283
285
|
return 'done';
|
|
284
|
-
const data = parseOpenAiSseData(frame.data);
|
|
286
|
+
const data = parseOpenAiSseData({ raw: frame.data });
|
|
285
287
|
if (!data)
|
|
286
288
|
return 'continue'; // malformed/empty keep-alive frame, or a non-object payload
|
|
287
|
-
applyOpenAiStreamUsage(state, data, onEvent);
|
|
288
|
-
const choice = firstOpenAiChoice(data);
|
|
289
|
+
applyOpenAiStreamUsage({ state: state, data: data, onEvent: onEvent });
|
|
290
|
+
const choice = firstOpenAiChoice({ data: data });
|
|
289
291
|
if (!choice)
|
|
290
292
|
return 'continue';
|
|
291
|
-
return handleOpenAiChoiceDelta(state, choice, onEvent) === 'break' ? 'contaminated' : 'continue';
|
|
293
|
+
return handleOpenAiChoiceDelta({ state: state, choice: choice, onEvent: onEvent }) === 'break' ? 'contaminated' : 'continue';
|
|
292
294
|
}
|
|
293
295
|
/**
|
|
294
296
|
* Runs exactly one OpenAI-compatible (Chat Completions JSON wire format)
|
|
@@ -305,24 +307,25 @@ export function processOpenAiStreamFrame(state, frame, onEvent) {
|
|
|
305
307
|
* `anthropic-messages.ts#runSingleAnthropicRequest`'s `emitEnd` contract —
|
|
306
308
|
* see that function's doc.
|
|
307
309
|
*/
|
|
308
|
-
export async function runOpenAiCompatibleRequest(
|
|
310
|
+
export async function runOpenAiCompatibleRequest(requiredArgs, optionalArgs = {}) {
|
|
311
|
+
const init = { ...optionalArgs, ...requiredArgs };
|
|
309
312
|
const { onEvent, hasEnded } = init;
|
|
310
313
|
const fetchOutcome = await requestOpenAiCompatibleStream(init);
|
|
311
314
|
if (fetchOutcome.type === 'ended') {
|
|
312
315
|
return { finishReason: null, toolCalls: [], text: '' };
|
|
313
316
|
}
|
|
314
317
|
if (fetchOutcome.type === 'retry') {
|
|
315
|
-
return runOpenAiCompatibleRequest(fetchOutcome.retryInit);
|
|
318
|
+
return (({ url, headers, body, redactSecretsList, guardMessageId, providerLabel, onEvent, emitEnd, hasEnded, ...optionalArgs }) => runOpenAiCompatibleRequest({ url, headers, body, redactSecretsList, guardMessageId, providerLabel, onEvent, emitEnd, hasEnded }, optionalArgs))(fetchOutcome.retryInit);
|
|
316
319
|
}
|
|
317
320
|
onEvent({ type: 'status', label: 'requesting' });
|
|
318
321
|
const state = {
|
|
319
|
-
guard: createRoleMarkerGuard(init.guardMessageId),
|
|
322
|
+
guard: createRoleMarkerGuard({ messageId: init.guardMessageId }),
|
|
320
323
|
toolCalls: new Map(),
|
|
321
324
|
fullText: '',
|
|
322
325
|
finishReason: null,
|
|
323
326
|
usage: null,
|
|
324
327
|
};
|
|
325
|
-
for await (const frame of decodeSseStream(fetchOutcome.body)) {
|
|
328
|
+
for await (const frame of decodeSseStream({ source: fetchOutcome.body })) {
|
|
326
329
|
// No `hasEnded()` re-check at the top of this loop: the only in-loop call to `emitEnd`
|
|
327
330
|
// (contamination, below) is immediately followed by `break`, and every other call site is a
|
|
328
331
|
// pre-loop early `return` — traced across all five call sites in this function and
|
|
@@ -330,7 +333,7 @@ export async function runOpenAiCompatibleRequest(init) {
|
|
|
330
333
|
// `anthropic-messages.ts#runSingleAnthropicRequest`. `hasEnded()` is still consulted once,
|
|
331
334
|
// after this loop, to decide whether pending tool_use events should still be emitted (a
|
|
332
335
|
// contaminating delta can arrive on a *later* chunk than the one that set `finish_reason`).
|
|
333
|
-
const result = processOpenAiStreamFrame(state, frame, onEvent);
|
|
336
|
+
const result = processOpenAiStreamFrame({ state: state, frame: frame, onEvent: onEvent });
|
|
334
337
|
if (result === 'done')
|
|
335
338
|
break;
|
|
336
339
|
if (result === 'contaminated') {
|
|
@@ -338,7 +341,7 @@ export async function runOpenAiCompatibleRequest(init) {
|
|
|
338
341
|
break;
|
|
339
342
|
}
|
|
340
343
|
}
|
|
341
|
-
const resolvedToolCalls = resolveOpenAiToolCalls(state.toolCalls);
|
|
344
|
+
const resolvedToolCalls = resolveOpenAiToolCalls({ pending: state.toolCalls });
|
|
342
345
|
if (state.finishReason === 'tool_calls' && !hasEnded()) {
|
|
343
346
|
emitPendingOpenAiToolUseEvents(resolvedToolCalls, onEvent);
|
|
344
347
|
}
|
|
@@ -350,16 +353,16 @@ async function runSingleOpenAiRequest(options, messages, emitEnd, hasEnded) {
|
|
|
350
353
|
// so `https://internal.example.com -> 10.0.0.5` passed it and this runner connected to private
|
|
351
354
|
// infrastructure on behalf of whoever supplied `baseUrl`. Matches what the Azure, Google and
|
|
352
355
|
// Ollama runners in this directory already did.
|
|
353
|
-
const baseUrlCheck = await validateBaseUrlResolved(options.baseUrl ?? DEFAULT_OPENAI_BASE_URL, options.dnsLookup ?? defaultDnsLookup);
|
|
356
|
+
const baseUrlCheck = await validateBaseUrlResolved({ baseUrl: options.baseUrl ?? DEFAULT_OPENAI_BASE_URL, lookup: options.dnsLookup ?? defaultDnsLookup });
|
|
354
357
|
if (baseUrlCheck.error) {
|
|
355
358
|
options.onEvent({ type: 'error', message: baseUrlCheck.error });
|
|
356
359
|
emitEnd('error');
|
|
357
360
|
return { finishReason: null, toolCalls: [], text: '' };
|
|
358
361
|
}
|
|
359
|
-
return runOpenAiCompatibleRequest({
|
|
360
|
-
url: openAiRequestUrl(options.baseUrl),
|
|
361
|
-
headers: openAiHeaders(options),
|
|
362
|
-
body: openAiRequestBody(options, messages),
|
|
362
|
+
return (({ url, headers, body, redactSecretsList, guardMessageId, providerLabel, onEvent, emitEnd, hasEnded, ...optionalArgs }) => runOpenAiCompatibleRequest({ url, headers, body, redactSecretsList, guardMessageId, providerLabel, onEvent, emitEnd, hasEnded }, optionalArgs))({
|
|
363
|
+
url: openAiRequestUrl({ baseUrl: options.baseUrl }),
|
|
364
|
+
headers: (({ apiKey, model, messages, onEvent, ...optionalArgs }) => openAiHeaders({ apiKey, model, messages, onEvent }, optionalArgs))(options),
|
|
365
|
+
body: openAiRequestBody({ options: options, messages: messages }),
|
|
363
366
|
...(baseUrlCheck.pinnedAddress ? { pinnedAddress: baseUrlCheck.pinnedAddress } : {}),
|
|
364
367
|
...(options.signal ? { signal: options.signal } : {}),
|
|
365
368
|
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
@@ -399,7 +402,7 @@ const OPENAI_DATA_URI_PATTERN = /^data:([^;,]+)(?:;charset=[^;,]+)?;base64,(.+)$
|
|
|
399
402
|
*
|
|
400
403
|
* @complexity O(1) — reads `data.length`, never decodes or parses the base64 payload.
|
|
401
404
|
*/
|
|
402
|
-
export function invalidOpenAiContentPartReason(part) {
|
|
405
|
+
export function invalidOpenAiContentPartReason({ part }) {
|
|
403
406
|
if (part.type === 'text')
|
|
404
407
|
return null;
|
|
405
408
|
const dataUriMatch = OPENAI_DATA_URI_PATTERN.exec(part.image_url.url);
|
|
@@ -427,14 +430,14 @@ export function invalidOpenAiContentPartReason(part) {
|
|
|
427
430
|
*
|
|
428
431
|
* @complexity O(n) in the number of content parts; O(1) per part (see `invalidOpenAiContentPartReason`).
|
|
429
432
|
*/
|
|
430
|
-
export function sanitizeOpenAiToolResult(result) {
|
|
433
|
+
export function sanitizeOpenAiToolResult({ result }) {
|
|
431
434
|
if (typeof result.content === 'string')
|
|
432
435
|
return { content: result.content, isError: false };
|
|
433
436
|
if (result.content.length > MAX_IMAGES_PER_OPENAI_TOOL_RESULT) {
|
|
434
437
|
return { content: `tool result rejected: exceeds the ${MAX_IMAGES_PER_OPENAI_TOOL_RESULT}-image-per-request guard (${result.content.length} parts)`, isError: true };
|
|
435
438
|
}
|
|
436
439
|
for (const part of result.content) {
|
|
437
|
-
const reason = invalidOpenAiContentPartReason(part);
|
|
440
|
+
const reason = invalidOpenAiContentPartReason({ part: part });
|
|
438
441
|
if (reason)
|
|
439
442
|
return { content: `tool result rejected: ${reason}`, isError: true };
|
|
440
443
|
}
|
|
@@ -452,7 +455,7 @@ export function sanitizeOpenAiToolResult(result) {
|
|
|
452
455
|
*
|
|
453
456
|
* @complexity O(n) in the number of content parts.
|
|
454
457
|
*/
|
|
455
|
-
export function splitOpenAiToolResultContent(content) {
|
|
458
|
+
export function splitOpenAiToolResultContent({ content }) {
|
|
456
459
|
if (typeof content === 'string')
|
|
457
460
|
return { toolMessageContent: content, imageParts: [] };
|
|
458
461
|
const textParts = content.filter((part) => part.type === 'text');
|
|
@@ -465,7 +468,7 @@ export function splitOpenAiToolResultContent(content) {
|
|
|
465
468
|
* instead proceed to execute the pending tool calls — mirrors
|
|
466
469
|
* `anthropic-messages.ts#anthropicLoopExitReason`'s pure decision/effect split.
|
|
467
470
|
*/
|
|
468
|
-
export function openAiLoopExitReason(outcome, toolTurns, maxToolTurns) {
|
|
471
|
+
export function openAiLoopExitReason({ outcome, toolTurns, maxToolTurns }) {
|
|
469
472
|
if (outcome.finishReason !== 'tool_calls' || outcome.toolCalls.length === 0)
|
|
470
473
|
return 'stop';
|
|
471
474
|
if (toolTurns >= maxToolTurns)
|
|
@@ -478,14 +481,14 @@ export function openAiLoopExitReason(outcome, toolTurns, maxToolTurns) {
|
|
|
478
481
|
* module doc's "Tool messages cannot carry an image" section for why the split exists and why the
|
|
479
482
|
* follow-up is assembled once per batch rather than per call.
|
|
480
483
|
*/
|
|
481
|
-
export async function executeOpenAiToolCalls(executeTool, calls, onEvent) {
|
|
484
|
+
export async function executeOpenAiToolCalls({ executeTool, calls, onEvent }) {
|
|
482
485
|
const toolResultMessages = [];
|
|
483
486
|
const followUpParts = [];
|
|
484
487
|
for (const call of calls) {
|
|
485
488
|
const result = await executeTool(call);
|
|
486
|
-
const sanitized = sanitizeOpenAiToolResult(result);
|
|
489
|
+
const sanitized = sanitizeOpenAiToolResult({ result: result });
|
|
487
490
|
onEvent({ type: 'tool_result', toolUseId: call.id, content: sanitized.content, isError: sanitized.isError });
|
|
488
|
-
const split = splitOpenAiToolResultContent(sanitized.content);
|
|
491
|
+
const split = splitOpenAiToolResultContent({ content: sanitized.content });
|
|
489
492
|
toolResultMessages.push({ role: 'tool', content: split.toolMessageContent, tool_call_id: call.id });
|
|
490
493
|
if (split.imageParts.length > 0) {
|
|
491
494
|
// Attribution label — without it, the model cannot tell this image apart from a human
|
|
@@ -498,11 +501,11 @@ export async function executeOpenAiToolCalls(executeTool, calls, onEvent) {
|
|
|
498
501
|
return { toolResultMessages, followUpParts };
|
|
499
502
|
}
|
|
500
503
|
/** Builds the assistant turn that records the model's pending tool calls in `messages` history — `content` falls back to `null` (never `''`) per the wire schema. */
|
|
501
|
-
export function buildOpenAiAssistantToolCallMessage(text, toolCalls) {
|
|
504
|
+
export function buildOpenAiAssistantToolCallMessage({ text, toolCalls }) {
|
|
502
505
|
return { role: 'assistant', content: text || null, tool_calls: toolCalls };
|
|
503
506
|
}
|
|
504
507
|
/** Appends the batch's `tool` messages plus, when present, the single labeled-image follow-up message — see module doc's "Tool messages cannot carry an image" section for why the follow-up is at most one message per batch. */
|
|
505
|
-
export function buildOpenAiToolExchangeMessages(toolResultMessages, followUpParts) {
|
|
508
|
+
export function buildOpenAiToolExchangeMessages({ toolResultMessages, followUpParts }) {
|
|
506
509
|
return [...toolResultMessages, ...(followUpParts.length > 0 ? [{ role: 'user', content: followUpParts }] : [])];
|
|
507
510
|
}
|
|
508
511
|
/**
|
|
@@ -511,7 +514,8 @@ export function buildOpenAiToolExchangeMessages(toolResultMessages, followUpPart
|
|
|
511
514
|
* function call. See `anthropic-messages.ts#runAnthropicToolTurn`'s doc for
|
|
512
515
|
* the shared event-stream/`ended`-flag contract this mirrors exactly.
|
|
513
516
|
*/
|
|
514
|
-
export async function runOpenAiToolTurn(
|
|
517
|
+
export async function runOpenAiToolTurn(requiredArgs, optionalArgs = {}) {
|
|
518
|
+
const options = { ...optionalArgs, ...requiredArgs };
|
|
515
519
|
const maxToolTurns = options.maxToolTurns ?? DEFAULT_MAX_TOOL_TURNS;
|
|
516
520
|
const executeTool = options.executeTool;
|
|
517
521
|
const endGuard = createTurnEndGuard(options.onEvent, (reason) => ({ type: 'end', reason }));
|
|
@@ -524,7 +528,7 @@ export async function runOpenAiToolTurn(options) {
|
|
|
524
528
|
lastFinishReason = outcome.finishReason;
|
|
525
529
|
if (endGuard.hasEnded())
|
|
526
530
|
break;
|
|
527
|
-
const exitReason = openAiLoopExitReason(outcome, toolTurns, maxToolTurns);
|
|
531
|
+
const exitReason = openAiLoopExitReason({ outcome: outcome, toolTurns: toolTurns, maxToolTurns: maxToolTurns });
|
|
528
532
|
if (exitReason) {
|
|
529
533
|
emitEnd(exitReason);
|
|
530
534
|
break;
|
|
@@ -539,11 +543,11 @@ export async function runOpenAiToolTurn(options) {
|
|
|
539
543
|
type: 'function',
|
|
540
544
|
function: { name: call.name, arguments: JSON.stringify(call.input) },
|
|
541
545
|
}));
|
|
542
|
-
const { toolResultMessages, followUpParts } = await executeOpenAiToolCalls(executeTool, outcome.toolCalls, options.onEvent);
|
|
546
|
+
const { toolResultMessages, followUpParts } = await executeOpenAiToolCalls({ executeTool: executeTool, calls: outcome.toolCalls, onEvent: options.onEvent });
|
|
543
547
|
messages = [
|
|
544
548
|
...messages,
|
|
545
|
-
buildOpenAiAssistantToolCallMessage(outcome.text, assistantToolCalls),
|
|
546
|
-
...buildOpenAiToolExchangeMessages(toolResultMessages, followUpParts),
|
|
549
|
+
buildOpenAiAssistantToolCallMessage({ text: outcome.text, toolCalls: assistantToolCalls }),
|
|
550
|
+
...buildOpenAiToolExchangeMessages({ toolResultMessages: toolResultMessages, followUpParts: followUpParts }),
|
|
547
551
|
];
|
|
548
552
|
}
|
|
549
553
|
return { finishReason: lastFinishReason, toolTurns };
|