@jini-ai/agent-runtime 0.2.1 → 0.3.1
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/README.md +2 -2
- package/dist/agent-protocol/acp/session.d.ts +206 -1
- package/dist/agent-protocol/acp/session.d.ts.map +1 -1
- package/dist/agent-protocol/acp/session.js +505 -350
- package/dist/agent-protocol/acp/session.js.map +1 -1
- package/dist/agent-protocol/core/json-line-stream.d.ts +53 -0
- package/dist/agent-protocol/core/json-line-stream.d.ts.map +1 -1
- package/dist/agent-protocol/core/json-line-stream.js +307 -255
- package/dist/agent-protocol/core/json-line-stream.js.map +1 -1
- package/dist/agent-protocol/pi-rpc/events.d.ts +22 -1
- package/dist/agent-protocol/pi-rpc/events.d.ts.map +1 -1
- package/dist/agent-protocol/pi-rpc/events.js +189 -153
- package/dist/agent-protocol/pi-rpc/events.js.map +1 -1
- package/dist/agent-protocol/pi-rpc/session.d.ts +39 -0
- package/dist/agent-protocol/pi-rpc/session.d.ts.map +1 -1
- package/dist/agent-protocol/pi-rpc/session.js +143 -107
- package/dist/agent-protocol/pi-rpc/session.js.map +1 -1
- package/dist/auth.js +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/claude-stream.d.ts +65 -2
- package/dist/claude-stream.d.ts.map +1 -1
- package/dist/claude-stream.js +569 -442
- package/dist/claude-stream.js.map +1 -1
- package/dist/defs/aider.d.ts.map +1 -1
- package/dist/defs/aider.js +13 -1
- package/dist/defs/aider.js.map +1 -1
- package/dist/defs/amp.d.ts.map +1 -1
- package/dist/defs/amp.js +16 -1
- package/dist/defs/amp.js.map +1 -1
- package/dist/defs/amr.d.ts +2 -1
- package/dist/defs/amr.d.ts.map +1 -1
- package/dist/defs/amr.js +12 -1
- package/dist/defs/amr.js.map +1 -1
- package/dist/defs/antigravity.d.ts +25 -13
- package/dist/defs/antigravity.d.ts.map +1 -1
- package/dist/defs/antigravity.js +190 -163
- package/dist/defs/antigravity.js.map +1 -1
- package/dist/defs/claude.d.ts +7 -0
- package/dist/defs/claude.d.ts.map +1 -1
- package/dist/defs/claude.js +61 -3
- package/dist/defs/claude.js.map +1 -1
- package/dist/defs/codebuddy.d.ts.map +1 -1
- package/dist/defs/codebuddy.js +9 -1
- package/dist/defs/codebuddy.js.map +1 -1
- package/dist/defs/codex.d.ts +1 -0
- package/dist/defs/codex.d.ts.map +1 -1
- package/dist/defs/codex.js +34 -0
- package/dist/defs/codex.js.map +1 -1
- package/dist/defs/copilot.d.ts.map +1 -1
- package/dist/defs/copilot.js +15 -1
- package/dist/defs/copilot.js.map +1 -1
- package/dist/defs/cursor-agent.d.ts.map +1 -1
- package/dist/defs/cursor-agent.js +24 -2
- package/dist/defs/cursor-agent.js.map +1 -1
- package/dist/defs/deepseek.d.ts.map +1 -1
- package/dist/defs/deepseek.js +12 -0
- package/dist/defs/deepseek.js.map +1 -1
- package/dist/defs/devin.d.ts +2 -1
- package/dist/defs/devin.d.ts.map +1 -1
- package/dist/defs/devin.js +12 -7
- package/dist/defs/devin.js.map +1 -1
- package/dist/defs/grok-build.d.ts.map +1 -1
- package/dist/defs/grok-build.js +10 -0
- package/dist/defs/grok-build.js.map +1 -1
- package/dist/defs/hermes.d.ts +1 -0
- package/dist/defs/hermes.d.ts.map +1 -1
- package/dist/defs/hermes.js +3 -0
- package/dist/defs/hermes.js.map +1 -1
- package/dist/defs/kilo.d.ts +1 -0
- package/dist/defs/kilo.d.ts.map +1 -1
- package/dist/defs/kilo.js +3 -0
- package/dist/defs/kilo.js.map +1 -1
- package/dist/defs/kimi.d.ts +1 -0
- package/dist/defs/kimi.d.ts.map +1 -1
- package/dist/defs/kimi.js +3 -0
- package/dist/defs/kimi.js.map +1 -1
- package/dist/defs/kiro.d.ts +1 -0
- package/dist/defs/kiro.d.ts.map +1 -1
- package/dist/defs/kiro.js +3 -0
- package/dist/defs/kiro.js.map +1 -1
- package/dist/defs/opencode.d.ts +4 -0
- package/dist/defs/opencode.d.ts.map +1 -1
- package/dist/defs/opencode.js +19 -0
- package/dist/defs/opencode.js.map +1 -1
- package/dist/defs/pi.d.ts +5 -1
- package/dist/defs/pi.d.ts.map +1 -1
- package/dist/defs/pi.js +16 -4
- package/dist/defs/pi.js.map +1 -1
- package/dist/defs/qoder.d.ts +1 -0
- package/dist/defs/qoder.d.ts.map +1 -1
- package/dist/defs/qoder.js +15 -0
- package/dist/defs/qoder.js.map +1 -1
- package/dist/defs/qwen.d.ts.map +1 -1
- package/dist/defs/qwen.js +9 -0
- package/dist/defs/qwen.js.map +1 -1
- package/dist/defs/reasonix.d.ts +5 -0
- package/dist/defs/reasonix.d.ts.map +1 -1
- package/dist/defs/reasonix.js +19 -6
- package/dist/defs/reasonix.js.map +1 -1
- package/dist/defs/shared.d.ts +31 -1
- package/dist/defs/shared.d.ts.map +1 -1
- package/dist/defs/shared.js +35 -0
- package/dist/defs/shared.js.map +1 -1
- package/dist/defs/trae-cli.d.ts +1 -0
- package/dist/defs/trae-cli.d.ts.map +1 -1
- package/dist/defs/trae-cli.js +3 -0
- package/dist/defs/trae-cli.js.map +1 -1
- package/dist/defs/vibe.d.ts +1 -0
- package/dist/defs/vibe.d.ts.map +1 -1
- package/dist/defs/vibe.js +3 -0
- package/dist/defs/vibe.js.map +1 -1
- package/dist/detection.d.ts +1 -1
- package/dist/detection.d.ts.map +1 -1
- package/dist/detection.js +7 -2
- package/dist/detection.js.map +1 -1
- package/dist/env.js +1 -1
- package/dist/executables.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/invocation.js +1 -1
- package/dist/json-event-stream.d.ts.map +1 -1
- package/dist/json-event-stream.js +63 -0
- package/dist/json-event-stream.js.map +1 -1
- package/dist/launch.js +1 -1
- package/dist/log-file.d.ts +23 -0
- package/dist/log-file.d.ts.map +1 -0
- package/dist/log-file.js +56 -0
- package/dist/log-file.js.map +1 -0
- package/dist/mcp.d.ts +1 -1
- package/dist/metadata.d.ts +1 -1
- package/dist/mmd-routes.js +1 -1
- package/dist/model-registry.d.ts +7 -45
- package/dist/model-registry.d.ts.map +1 -1
- package/dist/model-registry.js.map +1 -1
- package/dist/models.d.ts +1 -1
- package/dist/opencode-log.js +1 -1
- package/dist/paths.js +1 -1
- package/dist/prompt-augmenter.d.ts +1 -1
- package/dist/prompt-augmenter.js +1 -1
- package/dist/prompt-budget.d.ts +1 -1
- package/dist/prompt-file.js +1 -1
- package/dist/providers/anthropic-messages.d.ts +198 -6
- package/dist/providers/anthropic-messages.d.ts.map +1 -1
- package/dist/providers/anthropic-messages.js +318 -141
- package/dist/providers/anthropic-messages.js.map +1 -1
- package/dist/providers/azure-chat.d.ts +113 -3
- package/dist/providers/azure-chat.d.ts.map +1 -1
- package/dist/providers/azure-chat.js +114 -21
- package/dist/providers/azure-chat.js.map +1 -1
- package/dist/providers/connection-guard.d.ts +121 -1
- package/dist/providers/connection-guard.d.ts.map +1 -1
- package/dist/providers/connection-guard.js +205 -3
- package/dist/providers/connection-guard.js.map +1 -1
- package/dist/providers/connection-test.d.ts +62 -0
- package/dist/providers/connection-test.d.ts.map +1 -0
- package/dist/providers/connection-test.js +404 -0
- package/dist/providers/connection-test.js.map +1 -0
- package/dist/providers/elevenlabs.d.ts.map +1 -1
- package/dist/providers/elevenlabs.js +3 -2
- package/dist/providers/elevenlabs.js.map +1 -1
- package/dist/providers/google-messages.d.ts +208 -7
- package/dist/providers/google-messages.d.ts.map +1 -1
- package/dist/providers/google-messages.js +331 -105
- package/dist/providers/google-messages.js.map +1 -1
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/model-catalog.d.ts.map +1 -1
- package/dist/providers/model-catalog.js +21 -0
- package/dist/providers/model-catalog.js.map +1 -1
- package/dist/providers/ollama-chat.d.ts +190 -1
- package/dist/providers/ollama-chat.d.ts.map +1 -1
- package/dist/providers/ollama-chat.js +319 -110
- package/dist/providers/ollama-chat.js.map +1 -1
- package/dist/providers/openai-chat.d.ts +222 -14
- package/dist/providers/openai-chat.d.ts.map +1 -1
- package/dist/providers/openai-chat.js +377 -128
- package/dist/providers/openai-chat.js.map +1 -1
- package/dist/providers/sse-decode.d.ts +2 -2
- package/dist/providers/sse-decode.js +2 -2
- package/dist/qoder-stream.d.ts +1 -1
- package/dist/qoder-stream.js +1 -1
- package/dist/registry.d.ts +16 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +19 -1
- package/dist/registry.js.map +1 -1
- package/dist/resolution.js +1 -1
- package/dist/terminal-launch.js +1 -1
- package/dist/types.d.ts +181 -89
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -4
|
@@ -1,3 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module providers/azure-chat
|
|
3
|
+
*
|
|
4
|
+
* Azure OpenAI Chat Completions wire adapter + tool-loop turn-runner.
|
|
5
|
+
* Sibling of `anthropic-messages.ts`/`openai-chat.ts` — see those modules'
|
|
6
|
+
* headers for the shared design rationale. Azure OpenAI's chat-completions
|
|
7
|
+
* JSON request/response body is byte-identical to plain OpenAI's (same
|
|
8
|
+
* `messages`/`tools`/streaming-delta shape) — only the URL and the auth
|
|
9
|
+
* header differ — so this module reuses `openai-chat.ts`'s extracted
|
|
10
|
+
* `runOpenAiCompatibleRequest` SSE-reduction loop rather than duplicating
|
|
11
|
+
* it; see that function's doc for why it was pulled out.
|
|
12
|
+
*
|
|
13
|
+
* **URL**: `{baseUrl}/openai/deployments/{model}/chat/completions
|
|
14
|
+
* ?api-version={apiVersion}` — `model` here is the Azure *deployment name*,
|
|
15
|
+
* not a model id (Azure OpenAI resources map a deployment name to a
|
|
16
|
+
* specific model version at resource-creation time). `apiVersion` defaults
|
|
17
|
+
* to `'2024-10-21'`, the default this repo's real OD predecessor used for
|
|
18
|
+
* its own azure chat-completions proxy route (`apps/daemon/src/routes/
|
|
19
|
+
* chat.ts`'s `[proxy:azure]` handler, confirmed by reading that file
|
|
20
|
+
* directly in a sibling checkout — see `source-map.md`'s dated entry).
|
|
21
|
+
* That source additionally branches on whether `baseUrl` already contains
|
|
22
|
+
* a versioned `/openai/v1` path (a newer Azure OpenAI API preview); that
|
|
23
|
+
* branch is deliberately not carried forward here — this adapter targets
|
|
24
|
+
* the one documented, stable URL/body shape per this task's approved scope
|
|
25
|
+
* ("byte-identical to OpenAI, only URL/auth differ"). Its 400-retry
|
|
26
|
+
* behavior IS carried forward (see "Token-limit fix" below) — round-4
|
|
27
|
+
* external audit (`AUD-R4-001`) found the first port had dropped it,
|
|
28
|
+
* which fails every request against a GPT-5/o-series Azure deployment.
|
|
29
|
+
*
|
|
30
|
+
* **Auth**: `api-key: {apiKey}` header — NOT `Authorization: Bearer`,
|
|
31
|
+
* Azure OpenAI's own convention (also confirmed against the same OD
|
|
32
|
+
* source above).
|
|
33
|
+
*
|
|
34
|
+
* **No default `baseUrl`**: every Azure OpenAI resource has its own
|
|
35
|
+
* endpoint (`https://{resource}.openai.azure.com`), so unlike Anthropic/
|
|
36
|
+
* OpenAI/Google/Ollama, `baseUrl` is a required field on
|
|
37
|
+
* {@link AzureTurnOptions}, and `apiVersion` is validated as
|
|
38
|
+
* non-empty-if-supplied by the caller (`@jini-ai/http-kit`'s
|
|
39
|
+
* `parseAzureProxyRequest` — see that module).
|
|
40
|
+
*
|
|
41
|
+
* **Token-limit fix**: an earlier version of this module sent no token-limit
|
|
42
|
+
* field at all — a live comparison against a running Open Design daemon
|
|
43
|
+
* found OD's real `azure` proxy handler always sends `max_tokens` (defaulting
|
|
44
|
+
* to 8192), unconditionally the legacy field name — never the newer
|
|
45
|
+
* `max_completion_tokens` `openai-chat.ts` picks for GPT-5/o-series models,
|
|
46
|
+
* since Azure deployment names are caller-defined strings, not necessarily
|
|
47
|
+
* matching OpenAI's own model-naming scheme. Wired via
|
|
48
|
+
* `./token-params.js#buildLegacyMaxTokensParam`. Because deployment names
|
|
49
|
+
* are opaque, a legacy-field request can still be rejected by a
|
|
50
|
+
* GPT-5/o-series deployment with a 400 — OD's real handler retries exactly
|
|
51
|
+
* once with `max_completion_tokens` on that specific error
|
|
52
|
+
* (`isUnsupportedMaxTokensError`); this module does the same via
|
|
53
|
+
* `runOpenAiCompatibleRequest`'s `retryableBody` hook.
|
|
54
|
+
*
|
|
55
|
+
* **Image support inherits from plain OpenAI, confirmed by doc AND by code.** Microsoft's own
|
|
56
|
+
* vision how-to (`learn.microsoft.com/azure/ai-foundry/openai/how-to/gpt-with-vision`) states
|
|
57
|
+
* verbatim: "The format is the same as the chat completions API for GPT-4o, except that the
|
|
58
|
+
* message content can be an array containing text and images" — identical `type:'text'`/
|
|
59
|
+
* `type:'image_url'` shape and `detail` values as `openai-chat.ts`. That inheritance is genuine at
|
|
60
|
+
* the code level too, not just assumed from the docs matching: `azureRequestBody` below spreads
|
|
61
|
+
* `messages` straight into the JSON body with no per-field transform, so once `AzureMessageParam`
|
|
62
|
+
* (this module's own copy of `OpenAiMessageParam`, per this file's existing full-duplication
|
|
63
|
+
* convention — see every other type in this file) is widened the same way as OpenAI's, an image
|
|
64
|
+
* survives untouched all the way to the wire. `azure-chat.test.ts`'s new image tests assert the
|
|
65
|
+
* request body directly to prove this, rather than trusting the doc claim alone.
|
|
66
|
+
*
|
|
67
|
+
* **The synthetic follow-up message and its ordering/attribution rules are also inherited
|
|
68
|
+
* unchanged from `openai-chat.ts`** — same workaround for the same text-only `tool` message
|
|
69
|
+
* constraint, same requirement that every `tool` message in a batch be emitted before the single
|
|
70
|
+
* follow-up (never interleaved — a real 400 otherwise), same per-image attribution label naming
|
|
71
|
+
* the tool call it answers. See that module's doc for the full rationale; it is not repeated here
|
|
72
|
+
* beyond this pointer to avoid the two copies drifting.
|
|
73
|
+
*/
|
|
74
|
+
import { type DnsLookupFn, type PinnedFetch } from './connection-guard.js';
|
|
75
|
+
import { type OpenAiCompatibleRequestOutcome } from './openai-chat.js';
|
|
1
76
|
import { type TurnEndReason } from './turn-end-guard.js';
|
|
2
77
|
export interface AzureFunctionToolDef {
|
|
3
78
|
readonly type: 'function';
|
|
@@ -15,9 +90,23 @@ export interface AzureToolCallParam {
|
|
|
15
90
|
readonly arguments: string;
|
|
16
91
|
};
|
|
17
92
|
}
|
|
93
|
+
export interface AzureTextPart {
|
|
94
|
+
readonly type: 'text';
|
|
95
|
+
readonly text: string;
|
|
96
|
+
}
|
|
97
|
+
/** `detail` defaults to `'auto'` server-side when omitted — never sent unless the caller supplies it. Same shape as `openai-chat.ts#OpenAiImageUrlPart`; see module doc's "Image support inherits from plain OpenAI" note. */
|
|
98
|
+
export interface AzureImageUrlPart {
|
|
99
|
+
readonly type: 'image_url';
|
|
100
|
+
readonly image_url: {
|
|
101
|
+
readonly url: string;
|
|
102
|
+
readonly detail?: 'auto' | 'low' | 'high';
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** A user/system/assistant message's `content` array item. Not legal inside a `role: 'tool'` message — same constraint as OpenAI's (see `openai-chat.ts` module doc's "Tool messages cannot carry an image" section, which applies here identically). */
|
|
106
|
+
export type AzureContentPart = AzureTextPart | AzureImageUrlPart;
|
|
18
107
|
export interface AzureMessageParam {
|
|
19
108
|
readonly role: 'system' | 'user' | 'assistant' | 'tool';
|
|
20
|
-
readonly content: string | null;
|
|
109
|
+
readonly content: string | null | readonly AzureContentPart[];
|
|
21
110
|
readonly tool_calls?: readonly AzureToolCallParam[];
|
|
22
111
|
readonly tool_call_id?: string;
|
|
23
112
|
readonly name?: string;
|
|
@@ -27,8 +116,9 @@ export interface AzureToolCall {
|
|
|
27
116
|
readonly name: string;
|
|
28
117
|
readonly input: unknown;
|
|
29
118
|
}
|
|
119
|
+
/** `content` may include `AzureImageUrlPart`s (e.g. a vision self-check's screenshot) even though the wire `tool` message can't carry them directly — `runAzureToolTurn` splits them onto a follow-up `user` message; see `openai-chat.ts`'s identical design. */
|
|
30
120
|
export interface AzureToolResult {
|
|
31
|
-
readonly content: string;
|
|
121
|
+
readonly content: string | readonly AzureContentPart[];
|
|
32
122
|
}
|
|
33
123
|
/** Host-owned tool execution — same "the collaborator is always supplied" convention as `anthropic-messages.ts#AnthropicToolExecutor`. */
|
|
34
124
|
export type AzureToolExecutor = (call: AzureToolCall) => Promise<AzureToolResult>;
|
|
@@ -47,7 +137,7 @@ export type AzureTurnEvent = {
|
|
|
47
137
|
} | {
|
|
48
138
|
readonly type: 'tool_result';
|
|
49
139
|
readonly toolUseId: string;
|
|
50
|
-
readonly content: string;
|
|
140
|
+
readonly content: string | readonly AzureContentPart[];
|
|
51
141
|
readonly isError: boolean;
|
|
52
142
|
} | {
|
|
53
143
|
readonly type: 'usage';
|
|
@@ -84,11 +174,31 @@ export interface AzureTurnOptions {
|
|
|
84
174
|
readonly signal?: AbortSignal;
|
|
85
175
|
/** Caller-supplied extra headers — see `anthropic-messages.ts#AnthropicTurnOptions.extraHeaders`'s doc for why this exists and what it fixes. */
|
|
86
176
|
readonly extraHeaders?: Record<string, string>;
|
|
177
|
+
/** Injectable HTTP transport, defaulting to `connection-guard.ts`'s `pinnedFetch` (SSRF-guarded, DNS-pinned). See `anthropic-messages.ts#AnthropicTurnOptions.fetchImpl`'s doc for the seam this exists for and why production wiring must never override it. Forwarded to `openai-chat.ts#runOpenAiCompatibleRequest` — this provider delegates its actual request to that shared reducer (see module doc). */
|
|
178
|
+
readonly fetchImpl?: PinnedFetch;
|
|
179
|
+
/** Injectable DNS resolver for `validateBaseUrlResolved`'s DNS-pinning check, defaulting to `defaultDnsLookup`. Same test-only override rationale as `fetchImpl`. */
|
|
180
|
+
readonly dnsLookup?: DnsLookupFn;
|
|
87
181
|
}
|
|
88
182
|
export interface AzureTurnResult {
|
|
89
183
|
readonly finishReason: string | null;
|
|
90
184
|
readonly toolTurns: number;
|
|
91
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Decides why the tool loop should stop after one request, or returns `null` when it should
|
|
188
|
+
* instead proceed to execute the pending tool calls — mirrors
|
|
189
|
+
* `anthropic-messages.ts#anthropicLoopExitReason`'s pure decision/effect split.
|
|
190
|
+
*/
|
|
191
|
+
export declare function azureLoopExitReason(outcome: OpenAiCompatibleRequestOutcome, toolTurns: number, maxToolTurns: number): AzureTurnEndReason | null;
|
|
192
|
+
/** Builds the assistant continuation's `tool_calls` field — same shape as `openai-chat.ts#buildOpenAiAssistantToolCallMessage`'s `tool_calls`, since Azure's wire body is byte-identical to plain OpenAI's (see module doc). */
|
|
193
|
+
export declare function buildAzureAssistantToolCalls(toolCalls: readonly AzureToolCall[]): AzureToolCallParam[];
|
|
194
|
+
export interface AzureToolExecutionOutcome {
|
|
195
|
+
readonly toolResultMessages: AzureMessageParam[];
|
|
196
|
+
readonly followUpParts: AzureContentPart[];
|
|
197
|
+
}
|
|
198
|
+
/** Azure counterpart of `openai-chat.ts#executeOpenAiToolCalls` — same split (text-only `tool` message plus one labeled-image follow-up per batch) and same per-batch assembly discipline; see that function's doc and module doc's "synthetic follow-up message" note for why. */
|
|
199
|
+
export declare function executeAzureToolCalls(executeTool: AzureToolExecutor, calls: readonly AzureToolCall[], onEvent: (event: AzureTurnEvent) => void): Promise<AzureToolExecutionOutcome>;
|
|
200
|
+
/** Azure counterpart of `openai-chat.ts#buildOpenAiToolExchangeMessages` — appends the batch's `tool` messages plus, when present, the single labeled-image follow-up message. */
|
|
201
|
+
export declare function buildAzureToolExchangeMessages(toolResultMessages: readonly AzureMessageParam[], followUpParts: readonly AzureContentPart[]): AzureMessageParam[];
|
|
92
202
|
/**
|
|
93
203
|
* Runs a full Azure OpenAI Chat Completions turn, including the
|
|
94
204
|
* tool-execution loop when `options.executeTool` is supplied and the model
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"azure-chat.d.ts","sourceRoot":"","sources":["../../src/providers/azure-chat.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"azure-chat.d.ts","sourceRoot":"","sources":["../../src/providers/azure-chat.ts"],"names":[],"mappings":"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"}
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* endpoint (`https://{resource}.openai.azure.com`), so unlike Anthropic/
|
|
36
36
|
* OpenAI/Google/Ollama, `baseUrl` is a required field on
|
|
37
37
|
* {@link AzureTurnOptions}, and `apiVersion` is validated as
|
|
38
|
-
* non-empty-if-supplied by the caller (`@jini-ai/http`'s
|
|
38
|
+
* non-empty-if-supplied by the caller (`@jini-ai/http-kit`'s
|
|
39
39
|
* `parseAzureProxyRequest` — see that module).
|
|
40
40
|
*
|
|
41
41
|
* **Token-limit fix**: an earlier version of this module sent no token-limit
|
|
@@ -51,6 +51,25 @@
|
|
|
51
51
|
* once with `max_completion_tokens` on that specific error
|
|
52
52
|
* (`isUnsupportedMaxTokensError`); this module does the same via
|
|
53
53
|
* `runOpenAiCompatibleRequest`'s `retryableBody` hook.
|
|
54
|
+
*
|
|
55
|
+
* **Image support inherits from plain OpenAI, confirmed by doc AND by code.** Microsoft's own
|
|
56
|
+
* vision how-to (`learn.microsoft.com/azure/ai-foundry/openai/how-to/gpt-with-vision`) states
|
|
57
|
+
* verbatim: "The format is the same as the chat completions API for GPT-4o, except that the
|
|
58
|
+
* message content can be an array containing text and images" — identical `type:'text'`/
|
|
59
|
+
* `type:'image_url'` shape and `detail` values as `openai-chat.ts`. That inheritance is genuine at
|
|
60
|
+
* the code level too, not just assumed from the docs matching: `azureRequestBody` below spreads
|
|
61
|
+
* `messages` straight into the JSON body with no per-field transform, so once `AzureMessageParam`
|
|
62
|
+
* (this module's own copy of `OpenAiMessageParam`, per this file's existing full-duplication
|
|
63
|
+
* convention — see every other type in this file) is widened the same way as OpenAI's, an image
|
|
64
|
+
* survives untouched all the way to the wire. `azure-chat.test.ts`'s new image tests assert the
|
|
65
|
+
* request body directly to prove this, rather than trusting the doc claim alone.
|
|
66
|
+
*
|
|
67
|
+
* **The synthetic follow-up message and its ordering/attribution rules are also inherited
|
|
68
|
+
* unchanged from `openai-chat.ts`** — same workaround for the same text-only `tool` message
|
|
69
|
+
* constraint, same requirement that every `tool` message in a batch be emitted before the single
|
|
70
|
+
* follow-up (never interleaved — a real 400 otherwise), same per-image attribution label naming
|
|
71
|
+
* the tool call it answers. See that module's doc for the full rationale; it is not repeated here
|
|
72
|
+
* beyond this pointer to avoid the two copies drifting.
|
|
54
73
|
*/
|
|
55
74
|
import { defaultDnsLookup, validateBaseUrlResolved } from './connection-guard.js';
|
|
56
75
|
import { runOpenAiCompatibleRequest } from './openai-chat.js';
|
|
@@ -88,7 +107,7 @@ function azureRequestBody(options, messages, tokenParam) {
|
|
|
88
107
|
}
|
|
89
108
|
/** Validates `options.baseUrl`, then delegates to `openai-chat.ts#runOpenAiCompatibleRequest` with Azure's own URL/header/body builders. */
|
|
90
109
|
async function runSingleAzureRequest(options, messages, emitEnd, hasEnded) {
|
|
91
|
-
const baseUrlCheck = await validateBaseUrlResolved(options.baseUrl, defaultDnsLookup);
|
|
110
|
+
const baseUrlCheck = await validateBaseUrlResolved(options.baseUrl, options.dnsLookup ?? defaultDnsLookup);
|
|
92
111
|
if (baseUrlCheck.error) {
|
|
93
112
|
options.onEvent({ type: 'error', message: baseUrlCheck.error });
|
|
94
113
|
emitEnd('error');
|
|
@@ -102,7 +121,9 @@ async function runSingleAzureRequest(options, messages, emitEnd, hasEnded) {
|
|
|
102
121
|
retryableBody: (status, rawErrorText) => status === 400 && isUnsupportedMaxTokensError(rawErrorText)
|
|
103
122
|
? azureRequestBody(options, messages, buildMaxCompletionTokensParam(maxTokens))
|
|
104
123
|
: null,
|
|
124
|
+
...(baseUrlCheck.pinnedAddress ? { pinnedAddress: baseUrlCheck.pinnedAddress } : {}),
|
|
105
125
|
...(options.signal ? { signal: options.signal } : {}),
|
|
126
|
+
...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
|
|
106
127
|
redactSecretsList: [options.apiKey],
|
|
107
128
|
guardMessageId: 'azure-turn',
|
|
108
129
|
providerLabel: 'Azure OpenAI',
|
|
@@ -111,6 +132,89 @@ async function runSingleAzureRequest(options, messages, emitEnd, hasEnded) {
|
|
|
111
132
|
hasEnded,
|
|
112
133
|
});
|
|
113
134
|
}
|
|
135
|
+
const AZURE_ALLOWED_IMAGE_MEDIA_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif']);
|
|
136
|
+
/** Same 20 MB conservative single-image guard as `openai-chat.ts#MAX_IMAGE_DATA_URI_BASE64_CHARS` — see that constant's doc for the full rationale (Azure inherits OpenAI's image support, so it inherits this module's own defensive posture around it too). */
|
|
137
|
+
const MAX_IMAGE_DATA_URI_BASE64_CHARS = Math.ceil((20 * 1024 * 1024 * 4) / 3);
|
|
138
|
+
/** Same conservative per-tool-result image-count guard as `openai-chat.ts#MAX_IMAGES_PER_OPENAI_TOOL_RESULT`. */
|
|
139
|
+
const MAX_IMAGES_PER_AZURE_TOOL_RESULT = 1500;
|
|
140
|
+
const AZURE_DATA_URI_PATTERN = /^data:([^;,]+)(?:;charset=[^;,]+)?;base64,(.+)$/su;
|
|
141
|
+
/** Azure counterpart of `openai-chat.ts#invalidOpenAiContentPartReason` — same validation, same URL-sourced-image skip rationale (Azure's own servers fetch a plain `https://` `image_url`, not this adapter). @complexity O(1). */
|
|
142
|
+
function invalidAzureContentPartReason(part) {
|
|
143
|
+
if (part.type === 'text')
|
|
144
|
+
return null;
|
|
145
|
+
const dataUriMatch = AZURE_DATA_URI_PATTERN.exec(part.image_url.url);
|
|
146
|
+
if (!dataUriMatch)
|
|
147
|
+
return null;
|
|
148
|
+
const [, mediaType, base64Data] = dataUriMatch;
|
|
149
|
+
if (!mediaType || !AZURE_ALLOWED_IMAGE_MEDIA_TYPES.has(mediaType.toLowerCase())) {
|
|
150
|
+
return `unsupported image media type ${JSON.stringify(mediaType)} (Azure OpenAI's chat completions API supports image/jpeg, image/png, image/webp, and non-animated image/gif — same as plain OpenAI)`;
|
|
151
|
+
}
|
|
152
|
+
if (base64Data && base64Data.length > MAX_IMAGE_DATA_URI_BASE64_CHARS) {
|
|
153
|
+
return `image exceeds this adapter's 20 MB base64 size guard (${base64Data.length} base64 chars)`;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
/** Azure counterpart of `openai-chat.ts#sanitizeOpenAiToolResult` — same runtime guard, same rationale (an `AzureToolResult` is host-owned; the TS union only constrains a well-behaved host at compile time). @complexity O(n) in content parts. */
|
|
158
|
+
function sanitizeAzureToolResult(result) {
|
|
159
|
+
if (typeof result.content === 'string')
|
|
160
|
+
return { content: result.content, isError: false };
|
|
161
|
+
if (result.content.length > MAX_IMAGES_PER_AZURE_TOOL_RESULT) {
|
|
162
|
+
return { content: `tool result rejected: exceeds the ${MAX_IMAGES_PER_AZURE_TOOL_RESULT}-image-per-request guard (${result.content.length} parts)`, isError: true };
|
|
163
|
+
}
|
|
164
|
+
for (const part of result.content) {
|
|
165
|
+
const reason = invalidAzureContentPartReason(part);
|
|
166
|
+
if (reason)
|
|
167
|
+
return { content: `tool result rejected: ${reason}`, isError: true };
|
|
168
|
+
}
|
|
169
|
+
return { content: result.content, isError: false };
|
|
170
|
+
}
|
|
171
|
+
/** Azure counterpart of `openai-chat.ts#splitOpenAiToolResultContent` — same text/image split, same placeholder-when-image-only rule, same reason (a `role: 'tool'` message on Azure is the identical wire shape as plain OpenAI's, so it has the identical text-only constraint). @complexity O(n) in content parts. */
|
|
172
|
+
function splitAzureToolResultContent(content) {
|
|
173
|
+
if (typeof content === 'string')
|
|
174
|
+
return { toolMessageContent: content, imageParts: [] };
|
|
175
|
+
const textParts = content.filter((part) => part.type === 'text');
|
|
176
|
+
const imageParts = content.filter((part) => part.type === 'image_url');
|
|
177
|
+
const toolMessageContent = textParts.length > 0 ? textParts : [{ type: 'text', text: '(tool result included only non-text content; see the following message)' }];
|
|
178
|
+
return { toolMessageContent, imageParts };
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Decides why the tool loop should stop after one request, or returns `null` when it should
|
|
182
|
+
* instead proceed to execute the pending tool calls — mirrors
|
|
183
|
+
* `anthropic-messages.ts#anthropicLoopExitReason`'s pure decision/effect split.
|
|
184
|
+
*/
|
|
185
|
+
export function azureLoopExitReason(outcome, toolTurns, maxToolTurns) {
|
|
186
|
+
if (outcome.finishReason !== 'tool_calls' || outcome.toolCalls.length === 0)
|
|
187
|
+
return 'stop';
|
|
188
|
+
if (toolTurns >= maxToolTurns)
|
|
189
|
+
return 'max_tool_turns';
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
/** Builds the assistant continuation's `tool_calls` field — same shape as `openai-chat.ts#buildOpenAiAssistantToolCallMessage`'s `tool_calls`, since Azure's wire body is byte-identical to plain OpenAI's (see module doc). */
|
|
193
|
+
export function buildAzureAssistantToolCalls(toolCalls) {
|
|
194
|
+
return toolCalls.map((call) => ({ id: call.id, type: 'function', function: { name: call.name, arguments: JSON.stringify(call.input) } }));
|
|
195
|
+
}
|
|
196
|
+
/** Azure counterpart of `openai-chat.ts#executeOpenAiToolCalls` — same split (text-only `tool` message plus one labeled-image follow-up per batch) and same per-batch assembly discipline; see that function's doc and module doc's "synthetic follow-up message" note for why. */
|
|
197
|
+
export async function executeAzureToolCalls(executeTool, calls, onEvent) {
|
|
198
|
+
const toolResultMessages = [];
|
|
199
|
+
const followUpParts = [];
|
|
200
|
+
for (const call of calls) {
|
|
201
|
+
const result = await executeTool(call);
|
|
202
|
+
const sanitized = sanitizeAzureToolResult(result);
|
|
203
|
+
onEvent({ type: 'tool_result', toolUseId: call.id, content: sanitized.content, isError: sanitized.isError });
|
|
204
|
+
const split = splitAzureToolResultContent(sanitized.content);
|
|
205
|
+
toolResultMessages.push({ role: 'tool', content: split.toolMessageContent, tool_call_id: call.id });
|
|
206
|
+
if (split.imageParts.length > 0) {
|
|
207
|
+
// Attribution label — see `openai-chat.ts`'s identical note for why this matters.
|
|
208
|
+
followUpParts.push({ type: 'text', text: `Image output from tool \`${call.name}\` (tool_call_id: ${call.id}):` });
|
|
209
|
+
followUpParts.push(...split.imageParts);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return { toolResultMessages, followUpParts };
|
|
213
|
+
}
|
|
214
|
+
/** Azure counterpart of `openai-chat.ts#buildOpenAiToolExchangeMessages` — appends the batch's `tool` messages plus, when present, the single labeled-image follow-up message. */
|
|
215
|
+
export function buildAzureToolExchangeMessages(toolResultMessages, followUpParts) {
|
|
216
|
+
return [...toolResultMessages, ...(followUpParts.length > 0 ? [{ role: 'user', content: followUpParts }] : [])];
|
|
217
|
+
}
|
|
114
218
|
/**
|
|
115
219
|
* Runs a full Azure OpenAI Chat Completions turn, including the
|
|
116
220
|
* tool-execution loop when `options.executeTool` is supplied and the model
|
|
@@ -119,6 +223,7 @@ async function runSingleAzureRequest(options, messages, emitEnd, hasEnded) {
|
|
|
119
223
|
*/
|
|
120
224
|
export async function runAzureToolTurn(options) {
|
|
121
225
|
const maxToolTurns = options.maxToolTurns ?? DEFAULT_MAX_TOOL_TURNS;
|
|
226
|
+
const executeTool = options.executeTool;
|
|
122
227
|
const endGuard = createTurnEndGuard(options.onEvent, (reason) => ({ type: 'end', reason }));
|
|
123
228
|
const emitEnd = endGuard.emitEnd;
|
|
124
229
|
let messages = options.messages.slice();
|
|
@@ -129,34 +234,22 @@ export async function runAzureToolTurn(options) {
|
|
|
129
234
|
lastFinishReason = outcome.finishReason;
|
|
130
235
|
if (endGuard.hasEnded())
|
|
131
236
|
break;
|
|
132
|
-
|
|
133
|
-
|
|
237
|
+
const exitReason = azureLoopExitReason(outcome, toolTurns, maxToolTurns);
|
|
238
|
+
if (exitReason) {
|
|
239
|
+
emitEnd(exitReason);
|
|
134
240
|
break;
|
|
135
241
|
}
|
|
136
|
-
if (!
|
|
242
|
+
if (!executeTool) {
|
|
137
243
|
emitEnd('stop');
|
|
138
244
|
break;
|
|
139
245
|
}
|
|
140
|
-
if (toolTurns >= maxToolTurns) {
|
|
141
|
-
emitEnd('max_tool_turns');
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
246
|
toolTurns += 1;
|
|
145
|
-
const assistantToolCalls = outcome.toolCalls
|
|
146
|
-
|
|
147
|
-
type: 'function',
|
|
148
|
-
function: { name: call.name, arguments: JSON.stringify(call.input) },
|
|
149
|
-
}));
|
|
150
|
-
const toolResultMessages = [];
|
|
151
|
-
for (const call of outcome.toolCalls) {
|
|
152
|
-
const result = await options.executeTool(call);
|
|
153
|
-
options.onEvent({ type: 'tool_result', toolUseId: call.id, content: result.content, isError: false });
|
|
154
|
-
toolResultMessages.push({ role: 'tool', content: result.content, tool_call_id: call.id });
|
|
155
|
-
}
|
|
247
|
+
const assistantToolCalls = buildAzureAssistantToolCalls(outcome.toolCalls);
|
|
248
|
+
const { toolResultMessages, followUpParts } = await executeAzureToolCalls(executeTool, outcome.toolCalls, options.onEvent);
|
|
156
249
|
messages = [
|
|
157
250
|
...messages,
|
|
158
251
|
{ role: 'assistant', content: outcome.text || null, tool_calls: assistantToolCalls },
|
|
159
|
-
...toolResultMessages,
|
|
252
|
+
...buildAzureToolExchangeMessages(toolResultMessages, followUpParts),
|
|
160
253
|
];
|
|
161
254
|
}
|
|
162
255
|
return { finishReason: lastFinishReason, toolTurns };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"azure-chat.js","sourceRoot":"","sources":["../../src/providers/azure-chat.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"azure-chat.js","sourceRoot":"","sources":["../../src/providers/azure-chat.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,17 +16,63 @@
|
|
|
16
16
|
* or RFC1918/link-local/CGNAT/metadata-service addresses, and strip bearer
|
|
17
17
|
* tokens / API-key headers / `?key=` query values out of free-form text
|
|
18
18
|
* before it is logged or surfaced to a caller.
|
|
19
|
+
*
|
|
20
|
+
* Also exports `pinnedFetch` — a `node:https`/`node:http`-based POST that dials the exact address
|
|
21
|
+
* `validateBaseUrlResolved` already validated, instead of leaving the transport to re-resolve DNS
|
|
22
|
+
* independently when it connects. Added later than the four functions above, for the same
|
|
23
|
+
* dependency-free package; see `pinnedFetch`'s own doc for why `fetch()` itself cannot do this.
|
|
24
|
+
*
|
|
25
|
+
* ## Paired with `@jini-ai/ui`'s `utils/endpoint-policy.ts`
|
|
26
|
+
*
|
|
27
|
+
* That module carries a browser-safe copy of the SYNCHRONOUS half below —
|
|
28
|
+
* `isLoopbackApiHost`/`isBlockedExternalApiHostname`/`validateBaseUrl` — so the
|
|
29
|
+
* settings tabs can reject a bad endpoint as the operator types it. It is a
|
|
30
|
+
* copy rather than an import because `endpoint-policy.ts` itself has zero
|
|
31
|
+
* imports by design and this package is Node-only (`validateBaseUrlResolved`
|
|
32
|
+
* needs `node:dns`); see that file's header for the full reasoning.
|
|
33
|
+
*
|
|
34
|
+
* **If the block-list here changes, change it there too.** A divergence means
|
|
35
|
+
* the UI accepts an endpoint this guard then refuses — or, in the direction
|
|
36
|
+
* that actually matters, the UI accepts one this guard would have refused.
|
|
19
37
|
*/
|
|
20
38
|
export interface BaseUrlValidationResult {
|
|
21
39
|
parsed?: URL;
|
|
22
40
|
error?: string;
|
|
23
41
|
forbidden?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The validated address to pin the outbound connection to, present exactly when
|
|
44
|
+
* {@link validateBaseUrlResolved} performed a DNS lookup and passed it (i.e. never set by
|
|
45
|
+
* {@link validateBaseUrl} alone, and never set for the loopback-literal / IP-literal hosts that
|
|
46
|
+
* skip resolution — see that function's doc for why those two cases need no pinning). Feed this
|
|
47
|
+
* straight into {@link pinnedFetch} so the connection dials the exact address the guard approved
|
|
48
|
+
* instead of re-resolving DNS when it dials.
|
|
49
|
+
*/
|
|
50
|
+
pinnedAddress?: DnsLookupAddress;
|
|
24
51
|
}
|
|
25
52
|
/** True for `localhost`, `::1`, `127.0.0.0/8`, and their IPv4-mapped-IPv6 forms. */
|
|
26
53
|
export declare function isLoopbackApiHost(hostname: string): boolean;
|
|
27
54
|
/** True for RFC1918/link-local/CGNAT/multicast/unspecified/unique-local-IPv6 addresses — private network space a public caller should never be steered into. */
|
|
28
55
|
export declare function isBlockedExternalApiHostname(hostname: string): boolean;
|
|
29
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Synchronous base-URL check: scheme allow-list + literal-hostname block-list.
|
|
58
|
+
* Does not resolve DNS — see {@link validateBaseUrlResolved}.
|
|
59
|
+
*
|
|
60
|
+
* **Trims before parsing, and must keep doing so.** `@jini-ai/ui`'s
|
|
61
|
+
* `isAllowedEndpointUrl` is a deliberate browser-safe copy of this function and
|
|
62
|
+
* trims its input; this one did not. WHATWG URL parsing strips leading/trailing
|
|
63
|
+
* ASCII space itself, so the two agreed on `" https://x "` and diverged only on
|
|
64
|
+
* whitespace `String.prototype.trim` removes but the URL parser does not — NBSP,
|
|
65
|
+
* BOM, thin space and friends. A URL pasted from a rich-text source therefore
|
|
66
|
+
* passed the UI and was refused at connection time.
|
|
67
|
+
* `endpoint-policy.parity.test.ts` now holds the two in agreement mechanically.
|
|
68
|
+
*
|
|
69
|
+
* Trimming cannot weaken the block-list. Checked across every whitespace
|
|
70
|
+
* character `trim` strips, against blocked/loopback/public hosts in all three
|
|
71
|
+
* padding positions: an untrimmed parse either fails outright or yields a
|
|
72
|
+
* hostname IDENTICAL to the trimmed one. It never yields a DIFFERENT host, which
|
|
73
|
+
* is the only shape that would let the block-list inspect one string while the
|
|
74
|
+
* network used another.
|
|
75
|
+
*/
|
|
30
76
|
export declare function validateBaseUrl(baseUrl: string): BaseUrlValidationResult;
|
|
31
77
|
export type DnsLookupAddress = {
|
|
32
78
|
address: string;
|
|
@@ -45,6 +91,25 @@ export type DnsLookupFn = (hostname: string) => Promise<DnsLookupAddress[]>;
|
|
|
45
91
|
* hostname that resolves to a loopback address (including `*.localhost` per
|
|
46
92
|
* RFC 6761 and IPv4-mapped IPv6 loopback) follows the same carve-out.
|
|
47
93
|
*
|
|
94
|
+
* ## The TOCTOU this closes, and how
|
|
95
|
+
*
|
|
96
|
+
* A validator that only checks and returns pass/fail has a gap: `fetch` (or any transport) then
|
|
97
|
+
* resolves the hostname AGAIN, independently, when it dials. Between those two resolutions the
|
|
98
|
+
* answer can change — a DNS rebinding attacker returns a public address to this check and a
|
|
99
|
+
* private one to the connection. No amount of re-checking closes that on its own; the request has
|
|
100
|
+
* to dial the exact address that was approved. So this function does the resolution exactly ONCE
|
|
101
|
+
* and hands the first validated address back as `pinnedAddress` — feed it to {@link pinnedFetch},
|
|
102
|
+
* which dials that address directly instead of letting the transport re-resolve. `pinnedAddress` is
|
|
103
|
+
* only set when a lookup actually happened (i.e. never for the loopback-literal / IP-literal hosts
|
|
104
|
+
* below, which need no pinning: an IP literal never re-resolves to anything else, and loopback
|
|
105
|
+
* literals are outside this threat model — see the carve-out above).
|
|
106
|
+
*
|
|
107
|
+
* That said, `baseUrl` here is operator-configured provider config, not attacker-supplied input —
|
|
108
|
+
* pinning is defence-in-depth rather than the primary trust boundary. Callers should additionally
|
|
109
|
+
* refuse redirects (`redirect: 'error'`), which closes the OTHER way a validated origin reaches an
|
|
110
|
+
* unvalidated address; `pinnedFetch` never follows one regardless (`node:https`/`node:http` don't,
|
|
111
|
+
* unlike `fetch`'s default), but callers still pass `redirect: 'error'` for self-documentation.
|
|
112
|
+
*
|
|
48
113
|
* DNS lookup failures are not treated as a security signal — the caller is
|
|
49
114
|
* going to surface a connection error from `fetch` anyway, and turning a
|
|
50
115
|
* transient resolver hiccup into a rejection would just confuse callers. The
|
|
@@ -54,6 +119,61 @@ export type DnsLookupFn = (hostname: string) => Promise<DnsLookupAddress[]>;
|
|
|
54
119
|
* hosts that already own a resolver.
|
|
55
120
|
*/
|
|
56
121
|
export declare function validateBaseUrlResolved(baseUrl: string, lookup: DnsLookupFn): Promise<BaseUrlValidationResult>;
|
|
122
|
+
/** Request options accepted by {@link pinnedFetch} — the subset of `fetch`'s `init` this package's provider adapters actually pass. `redirect` is accepted only as documentation of intent: `pinnedFetch` never follows a redirect regardless of this field, so a caller cannot opt back into `fetch`'s default follow-redirects behavior. */
|
|
123
|
+
export interface PinnedFetchInit {
|
|
124
|
+
readonly method: string;
|
|
125
|
+
readonly headers: Record<string, string>;
|
|
126
|
+
readonly body: string;
|
|
127
|
+
readonly redirect?: 'error';
|
|
128
|
+
readonly signal?: AbortSignal;
|
|
129
|
+
}
|
|
130
|
+
/** The subset of `fetch`'s `Response` this package's provider adapters actually consume. */
|
|
131
|
+
export interface PinnedFetchResponse {
|
|
132
|
+
readonly ok: boolean;
|
|
133
|
+
readonly status: number;
|
|
134
|
+
readonly body: AsyncIterable<Uint8Array> | null;
|
|
135
|
+
text(): Promise<string>;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The call signature of {@link pinnedFetch} — exported so provider adapters
|
|
139
|
+
* (`AnthropicTurnOptions.fetchImpl`, `OpenAiTurnOptions.fetchImpl`, etc.) can accept it as an
|
|
140
|
+
* injectable dependency instead of hardcoding the import, and so a test can pass a fake directly
|
|
141
|
+
* through that seam rather than module-mocking this file (or, worse, its compiled `dist/` output —
|
|
142
|
+
* see `@jini-ai/http-kit`'s `model-proxy.test.ts` for the fragility that motivated this). Every
|
|
143
|
+
* adapter still defaults to the real {@link pinnedFetch} when the option is omitted — production
|
|
144
|
+
* behavior, including the SSRF guard and DNS pinning, never changes unless a caller explicitly
|
|
145
|
+
* overrides it.
|
|
146
|
+
*/
|
|
147
|
+
export type PinnedFetch = (url: string, init: PinnedFetchInit, pinnedAddress: DnsLookupAddress | undefined) => Promise<PinnedFetchResponse>;
|
|
148
|
+
/**
|
|
149
|
+
* `fetch()`-shaped POST that dials `pinnedAddress` directly instead of letting the transport
|
|
150
|
+
* re-resolve DNS when it connects — see {@link validateBaseUrlResolved}'s doc for the TOCTOU this
|
|
151
|
+
* closes and why `pinnedAddress` is the exact address that function already validated.
|
|
152
|
+
*
|
|
153
|
+
* Built on `node:https`/`node:http`, not `fetch`, because Node's global `fetch` is backed by an
|
|
154
|
+
* internal, non-importable copy of undici and exposes no `lookup`/dispatcher hook without adding
|
|
155
|
+
* `undici` (or an equivalent) as a runtime dependency — confirmed empirically against this
|
|
156
|
+
* package's pinned Node version (`require('node:undici')` throws `No such built-in module`, i.e.
|
|
157
|
+
* it is not a stable built-in here even though Node's own `fetch` uses it internally). This
|
|
158
|
+
* package carries no external runtime dependencies by design (module doc), so `node:https`/
|
|
159
|
+
* `node:http`'s own `lookup` option — which `net.connect` already accepts for exactly this purpose
|
|
160
|
+
* — is the only built-in path.
|
|
161
|
+
*
|
|
162
|
+
* `hostname`/`servername` passed to the transport stay the ORIGINAL host from `url`, not
|
|
163
|
+
* `pinnedAddress`; only the `lookup` override changes which address the socket actually dials.
|
|
164
|
+
* That is what keeps this a pin rather than a redirect to a different origin: the Host header and,
|
|
165
|
+
* for https, the TLS SNI + certificate hostname validation are unaffected, so a certificate for the
|
|
166
|
+
* real hostname still validates normally against a connection that happens to land on the address
|
|
167
|
+
* the guard already approved. Never follows a redirect — `http(s).request` doesn't, unlike
|
|
168
|
+
* `fetch`'s default — which is the other half of the rebinding surface `redirect: 'error'`
|
|
169
|
+
* documents at each call site.
|
|
170
|
+
*
|
|
171
|
+
* When `pinnedAddress` is `undefined` — the loopback-literal / IP-literal hosts
|
|
172
|
+
* `validateBaseUrlResolved` never runs a lookup for — this issues a normal, unpinned request
|
|
173
|
+
* (`lookup` option simply omitted). That is intentional, not a gap: an IP literal has nothing left
|
|
174
|
+
* to re-resolve, and loopback literals are the documented carve-out, outside this threat model.
|
|
175
|
+
*/
|
|
176
|
+
export declare function pinnedFetch(url: string, init: PinnedFetchInit, pinnedAddress: DnsLookupAddress | undefined): Promise<PinnedFetchResponse>;
|
|
57
177
|
/**
|
|
58
178
|
* Redacts bearer tokens, `x-api-key`/`api-key`/`x-goog-api-key` headers,
|
|
59
179
|
* `?key=` query values, and any exact secret strings supplied via
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection-guard.d.ts","sourceRoot":"","sources":["../../src/providers/connection-guard.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"connection-guard.d.ts","sourceRoot":"","sources":["../../src/providers/connection-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,gBAAgB,CAAC;CAClC;AAsED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAM3D;AAED,gKAAgK;AAChK,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAQtE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,uBAAuB,CAiBxE;AAED,MAAM,MAAM,gBAAgB,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AACnE,MAAM,MAAM,WAAW,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAC;AAU5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,uBAAuB,CAAC,CA+BlC;AAED,8UAA8U;AAC9U,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,4FAA4F;AAC5F,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC;IAChD,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG,CACxB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,eAAe,EACrB,aAAa,EAAE,gBAAgB,GAAG,SAAS,KACxC,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAYlC;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,eAAe,EACrB,aAAa,EAAE,gBAAgB,GAAG,SAAS,GAC1C,OAAO,CAAC,mBAAmB,CAAC,CAsH9B;AAMD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,YAAY,GAAE,aAAa,CAAC,MAAM,GAAG,SAAS,GAAG,IAAI,CAAM,GAC1D,MAAM,CAWR;AAED,kIAAkI;AAClI,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAIpF"}
|