@oh-my-pi/pi-ai 18.4.2 → 18.4.4
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 +30 -41
- package/dist/types/auth-broker/protocol.d.ts +12 -0
- package/dist/types/dialect/rendering.d.ts +4 -0
- package/dist/types/images/openai-hosted.d.ts +1 -1
- package/dist/types/images/shared.d.ts +5 -2
- package/dist/types/providers/anthropic-wire.d.ts +9 -1
- package/dist/types/providers/anthropic.d.ts +17 -0
- package/dist/types/providers/aws-sigv4.d.ts +5 -0
- package/dist/types/providers/bedrock-anthropic.d.ts +9 -0
- package/dist/types/providers/bedrock-request-metadata.d.ts +2 -0
- package/dist/types/providers/cursor/interaction-query.d.ts +10 -0
- package/dist/types/providers/openai-chat-wire.d.ts +2 -2
- package/dist/types/providers/openai-codex/request-transformer.d.ts +1 -1
- package/dist/types/providers/openai-responses-wire.d.ts +2 -2
- package/dist/types/providers/xai-base-url.d.ts +17 -0
- package/dist/types/types.d.ts +14 -3
- package/dist/types/usage/commandcode.d.ts +4 -0
- package/dist/types/usage/shared.d.ts +13 -1
- package/dist/types/utils/event-stream.d.ts +7 -0
- package/dist/types/utils/openai-http.d.ts +7 -1
- package/package.json +6 -6
- package/src/auth-broker/client.ts +1 -12
- package/src/auth-broker/protocol.ts +32 -0
- package/src/auth-broker/remote-store.ts +4 -40
- package/src/auth-broker/server.ts +1 -20
- package/src/auth-broker/snapshot-cache.ts +1 -9
- package/src/dialect/anthropic.ts +3 -25
- package/src/dialect/minimax.ts +3 -24
- package/src/dialect/rendering.ts +18 -0
- package/src/dialect/thinking.ts +24 -6
- package/src/dialect/xml.ts +3 -19
- package/src/images/openai-hosted.ts +5 -2
- package/src/images/openai-images.ts +10 -4
- package/src/images/shared.ts +9 -4
- package/src/providers/amazon-bedrock.ts +61 -22
- package/src/providers/anthropic-compaction.ts +10 -1
- package/src/providers/anthropic-wire.ts +12 -1
- package/src/providers/anthropic.ts +58 -15
- package/src/providers/aws-eventstream.ts +4 -3
- package/src/providers/aws-sigv4.ts +1 -1
- package/src/providers/azure-openai-responses.ts +3 -4
- package/src/providers/bedrock-anthropic.ts +30 -0
- package/src/providers/bedrock-request-metadata.ts +6 -0
- package/src/providers/connect-error-detail.ts +1 -5
- package/src/providers/cursor/interaction-query.ts +4 -2
- package/src/providers/cursor.ts +30 -26
- package/src/providers/google-gemini-cli.ts +9 -2
- package/src/providers/google-shared.ts +16 -6
- package/src/providers/openai-chat-wire.ts +2 -2
- package/src/providers/openai-codex/request-transformer.ts +1 -1
- package/src/providers/openai-codex-responses.ts +27 -16
- package/src/providers/openai-completions.ts +51 -36
- package/src/providers/openai-responses-wire.ts +2 -2
- package/src/providers/openai-responses.ts +3 -4
- package/src/providers/openai-shared.ts +130 -18
- package/src/providers/xai-base-url.ts +32 -0
- package/src/registry/engine/api-key.ts +8 -3
- package/src/types.ts +35 -4
- package/src/usage/claude.ts +4 -11
- package/src/usage/cline-pass.ts +2 -14
- package/src/usage/commandcode.ts +209 -0
- package/src/usage/cursor.ts +9 -1
- package/src/usage/openai-codex.ts +3 -5
- package/src/usage/registry.ts +3 -0
- package/src/usage/shared.ts +28 -1
- package/src/usage/synthetic.ts +4 -40
- package/src/usage/umans.ts +8 -36
- package/src/usage/zai.ts +10 -38
- package/src/utils/event-stream.ts +38 -2
- package/src/utils/http-inspector.ts +4 -8
- package/src/utils/openai-http.ts +10 -3
- package/src/utils/schema/json-schema-validator.ts +23 -26
- package/src/utils/schema/meta-validator.ts +4 -7
- package/src/utils/schema/wire.ts +17 -21
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import * as fs from "node:fs/promises";
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
12
|
+
import { asStrict } from "../providers/aws-sigv4";
|
|
12
13
|
import type { SnapshotResponse } from "./types";
|
|
13
14
|
|
|
14
15
|
const MAGIC = new Uint8Array([0x4f, 0x4d, 0x50, 0x53]); // "OMPS"
|
|
@@ -210,15 +211,6 @@ async function deriveAesKey(token: string, usages: Array<"encrypt" | "decrypt">)
|
|
|
210
211
|
return globalThis.crypto.subtle.importKey("raw", digest, AES_ALGORITHM, false, usages);
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
function asStrict(bytes: Uint8Array): Uint8Array<ArrayBuffer> {
|
|
214
|
-
if (bytes.buffer instanceof ArrayBuffer && bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) {
|
|
215
|
-
return bytes as Uint8Array<ArrayBuffer>;
|
|
216
|
-
}
|
|
217
|
-
const copy = new Uint8Array(bytes.byteLength);
|
|
218
|
-
copy.set(bytes);
|
|
219
|
-
return copy;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
214
|
function randomHex(byteLength: number): string {
|
|
223
215
|
const bytes = new Uint8Array(byteLength);
|
|
224
216
|
globalThis.crypto.getRandomValues(bytes);
|
package/src/dialect/anthropic.ts
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
import { parseJsonWithRepair } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { escapeXmlText, parseJsonWithRepair } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { Message, ToolCall } from "../types";
|
|
3
3
|
import dialectPrompt from "./anthropic.md" with { type: "text" };
|
|
4
|
-
import { buildArgShapes, buildStringArgsResolver, mintToolCallId
|
|
5
|
-
import {
|
|
6
|
-
escapeXmlAttr,
|
|
7
|
-
escapeXmlText,
|
|
8
|
-
renderDelimitedThinking,
|
|
9
|
-
renderLegacyTextTranscript,
|
|
10
|
-
stringifyJson,
|
|
11
|
-
} from "./rendering";
|
|
4
|
+
import { buildArgShapes, buildStringArgsResolver, mintToolCallId } from "./coercion";
|
|
5
|
+
import { renderDelimitedThinking, renderInvoke, renderInvokes, renderLegacyTextTranscript } from "./rendering";
|
|
12
6
|
import type {
|
|
13
7
|
DialectDefinition,
|
|
14
8
|
DialectRenderOptions,
|
|
@@ -578,22 +572,6 @@ function renderTranscript(messages: readonly Message[], options: DialectRenderOp
|
|
|
578
572
|
});
|
|
579
573
|
}
|
|
580
574
|
|
|
581
|
-
function renderInvoke(call: ToolCall, shape: ToolArgShape | undefined): string {
|
|
582
|
-
let body = `<invoke name="${escapeXmlAttr(call.name)}">`;
|
|
583
|
-
for (const key in call.arguments) {
|
|
584
|
-
const value = call.arguments[key];
|
|
585
|
-
const isString = shape?.stringArgs.has(key) === true;
|
|
586
|
-
const rendered = isString && typeof value === "string" ? value : stringifyJson(value);
|
|
587
|
-
body += `<parameter name="${escapeXmlAttr(key)}">${rendered}</parameter>`;
|
|
588
|
-
}
|
|
589
|
-
return `${body}</invoke>`;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
function renderInvokes(calls: readonly ToolCall[], tools: NonNullable<DialectRenderOptions["tools"]>): string {
|
|
593
|
-
const shapes = buildArgShapes(tools);
|
|
594
|
-
return calls.map(call => renderInvoke(call, shapes.get(call.name))).join("\n");
|
|
595
|
-
}
|
|
596
|
-
|
|
597
575
|
const definition: DialectDefinition = {
|
|
598
576
|
dialect: "anthropic",
|
|
599
577
|
prompt: dialectPrompt,
|
package/src/dialect/minimax.ts
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
|
+
import { escapeXmlText } from "@oh-my-pi/pi-utils";
|
|
1
2
|
import type { Message, ToolCall } from "../types";
|
|
2
3
|
import {
|
|
3
4
|
ANTHROPIC_THINKING_TAG_PREFIXES,
|
|
4
5
|
AnthropicInbandScanner,
|
|
5
6
|
type AnthropicInbandScannerConfig,
|
|
6
7
|
} from "./anthropic";
|
|
7
|
-
import { buildArgShapes
|
|
8
|
+
import { buildArgShapes } from "./coercion";
|
|
8
9
|
import dialectPrompt from "./minimax.md" with { type: "text" };
|
|
9
|
-
import {
|
|
10
|
-
escapeXmlAttr,
|
|
11
|
-
escapeXmlText,
|
|
12
|
-
renderDelimitedThinking,
|
|
13
|
-
renderLegacyTextTranscript,
|
|
14
|
-
stringifyJson,
|
|
15
|
-
} from "./rendering";
|
|
10
|
+
import { renderDelimitedThinking, renderInvoke, renderInvokes, renderLegacyTextTranscript } from "./rendering";
|
|
16
11
|
import type { DialectDefinition, DialectRenderOptions, DialectToolResult } from "./types";
|
|
17
12
|
|
|
18
13
|
const MINIMAX_WRAPPER_TAGS: Readonly<Record<string, true>> = { tool_call: true };
|
|
@@ -65,22 +60,6 @@ function renderTranscript(messages: readonly Message[], options: DialectRenderOp
|
|
|
65
60
|
});
|
|
66
61
|
}
|
|
67
62
|
|
|
68
|
-
function renderInvoke(call: ToolCall, shape: ToolArgShape | undefined): string {
|
|
69
|
-
let body = `<invoke name="${escapeXmlAttr(call.name)}">`;
|
|
70
|
-
for (const key in call.arguments) {
|
|
71
|
-
const value = call.arguments[key];
|
|
72
|
-
const isString = shape?.stringArgs.has(key) === true;
|
|
73
|
-
const rendered = isString && typeof value === "string" ? value : stringifyJson(value);
|
|
74
|
-
body += `<parameter name="${escapeXmlAttr(key)}">${rendered}</parameter>`;
|
|
75
|
-
}
|
|
76
|
-
return `${body}</invoke>`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function renderInvokes(calls: readonly ToolCall[], tools: NonNullable<DialectRenderOptions["tools"]>): string {
|
|
80
|
-
const shapes = buildArgShapes(tools);
|
|
81
|
-
return calls.map(call => renderInvoke(call, shapes.get(call.name))).join("\n");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
63
|
const definition: DialectDefinition = {
|
|
85
64
|
dialect: "minimax",
|
|
86
65
|
prompt: dialectPrompt,
|
package/src/dialect/rendering.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { stringifyJson as stringifyJsonValue } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { AssistantMessage, Message, ToolCall, ToolResultMessage } from "../types";
|
|
3
|
+
import { buildArgShapes, type ToolArgShape } from "./coercion";
|
|
3
4
|
import type { DialectRenderOptions, DialectToolResult } from "./types";
|
|
4
5
|
|
|
5
6
|
export function renderToolResponseResults(results: readonly DialectToolResult[]): string {
|
|
@@ -81,6 +82,23 @@ export function escapeXmlText(value: string): string {
|
|
|
81
82
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
/** Render one Anthropic-style `<invoke>`; declared string args stay raw, everything else is JSON. */
|
|
86
|
+
export function renderInvoke(call: ToolCall, shape: ToolArgShape | undefined): string {
|
|
87
|
+
let body = `<invoke name="${escapeXmlAttr(call.name)}">`;
|
|
88
|
+
for (const key in call.arguments) {
|
|
89
|
+
const value = call.arguments[key];
|
|
90
|
+
const isString = shape?.stringArgs.has(key) === true;
|
|
91
|
+
const rendered = isString && typeof value === "string" ? value : stringifyJson(value);
|
|
92
|
+
body += `<parameter name="${escapeXmlAttr(key)}">${rendered}</parameter>`;
|
|
93
|
+
}
|
|
94
|
+
return `${body}</invoke>`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function renderInvokes(calls: readonly ToolCall[], tools: NonNullable<DialectRenderOptions["tools"]>): string {
|
|
98
|
+
const shapes = buildArgShapes(tools);
|
|
99
|
+
return calls.map(call => renderInvoke(call, shapes.get(call.name))).join("\n");
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
export type AssistantTranscriptParts = {
|
|
85
103
|
readonly text: string;
|
|
86
104
|
readonly thinking: string;
|
package/src/dialect/thinking.ts
CHANGED
|
@@ -36,6 +36,18 @@ const TAGS: readonly Tag[] = [
|
|
|
36
36
|
const OPENS = TAGS.map(tag => tag.open);
|
|
37
37
|
const IMPLIED_OPEN_TAGS = TAGS.filter(tag => tag.impliedOpen);
|
|
38
38
|
const IMPLIED_OPEN_DELIMITERS = [...OPENS, ...IMPLIED_OPEN_TAGS.map(tag => tag.close)];
|
|
39
|
+
/** A hold needs the buffer tail to be a proper prefix of some delimiter, so it must be shorter than this. */
|
|
40
|
+
const MAX_DELIMITER_LENGTH = Math.max(...IMPLIED_OPEN_DELIMITERS.map(delimiter => delimiter.length));
|
|
41
|
+
const BACKTICK = 0x60;
|
|
42
|
+
const BOUNDARY_LEAD_CODES = [...IMPLIED_OPEN_DELIMITERS.map(delimiter => delimiter.charCodeAt(0)), BACKTICK];
|
|
43
|
+
/**
|
|
44
|
+
* `1` at the char code of every character a {@link scanVisible} boundary can
|
|
45
|
+
* start on: the first character of any delimiter (a tag open, an implied close,
|
|
46
|
+
* or a partial of either) and the backtick. Every other character — including
|
|
47
|
+
* any code past the table — is skipped without comparing.
|
|
48
|
+
*/
|
|
49
|
+
const BOUNDARY_LEAD = new Uint8Array(Math.max(...BOUNDARY_LEAD_CODES) + 1);
|
|
50
|
+
for (const code of BOUNDARY_LEAD_CODES) BOUNDARY_LEAD[code] = 1;
|
|
39
51
|
|
|
40
52
|
export interface ThinkingInbandScannerOptions {
|
|
41
53
|
/**
|
|
@@ -240,20 +252,26 @@ type VisibleHit =
|
|
|
240
252
|
*/
|
|
241
253
|
function scanVisible(buffer: string, final: boolean, impliedOpen: boolean): VisibleHit {
|
|
242
254
|
const delimiters = impliedOpen ? IMPLIED_OPEN_DELIMITERS : OPENS;
|
|
255
|
+
// Only the tail can be a proper prefix of a delimiter.
|
|
256
|
+
const holdFrom = final ? buffer.length : buffer.length - MAX_DELIMITER_LENGTH + 1;
|
|
243
257
|
for (let i = 0; i < buffer.length; i++) {
|
|
244
|
-
const
|
|
245
|
-
if (
|
|
258
|
+
const code = buffer.charCodeAt(i);
|
|
259
|
+
if (code >= BOUNDARY_LEAD.length || BOUNDARY_LEAD[code] === 0) continue;
|
|
260
|
+
for (const tag of TAGS) {
|
|
261
|
+
if (buffer.startsWith(tag.open, i)) return { kind: "tag", index: i, tag };
|
|
262
|
+
}
|
|
246
263
|
if (impliedOpen) {
|
|
247
|
-
const
|
|
248
|
-
|
|
264
|
+
for (const tag of IMPLIED_OPEN_TAGS) {
|
|
265
|
+
if (buffer.startsWith(tag.close, i)) return { kind: "impliedClose", index: i, tag };
|
|
266
|
+
}
|
|
249
267
|
}
|
|
250
|
-
if (
|
|
268
|
+
if (i >= holdFrom) {
|
|
251
269
|
const rest = buffer.slice(i);
|
|
252
270
|
if (delimiters.some(delimiter => delimiter.length > rest.length && delimiter.startsWith(rest))) {
|
|
253
271
|
return { kind: "hold", index: i };
|
|
254
272
|
}
|
|
255
273
|
}
|
|
256
|
-
if (
|
|
274
|
+
if (code === BACKTICK) {
|
|
257
275
|
const ticks = backtickRun(buffer, i);
|
|
258
276
|
if (!final && i + ticks === buffer.length) return { kind: "hold", index: i };
|
|
259
277
|
return { kind: "code", index: i, ticks };
|
package/src/dialect/xml.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { Message, ToolCall } from "../types";
|
|
2
2
|
import { AnthropicInbandScanner } from "./anthropic";
|
|
3
|
-
import { buildArgShapes
|
|
3
|
+
import { buildArgShapes } from "./coercion";
|
|
4
4
|
import { DeepSeekInbandScanner } from "./deepseek";
|
|
5
5
|
import {
|
|
6
|
-
escapeXmlAttr,
|
|
7
6
|
renderDelimitedThinking,
|
|
7
|
+
renderInvoke,
|
|
8
|
+
renderInvokes,
|
|
8
9
|
renderLegacyTextTranscript,
|
|
9
10
|
renderToolResponseResults,
|
|
10
|
-
stringifyJson,
|
|
11
11
|
} from "./rendering";
|
|
12
12
|
import type {
|
|
13
13
|
DialectDefinition,
|
|
@@ -60,22 +60,6 @@ function renderTranscript(messages: readonly Message[], options: DialectRenderOp
|
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function renderInvoke(call: ToolCall, shape: ToolArgShape | undefined): string {
|
|
64
|
-
let body = `<invoke name="${escapeXmlAttr(call.name)}">`;
|
|
65
|
-
for (const key in call.arguments) {
|
|
66
|
-
const value = call.arguments[key];
|
|
67
|
-
const isString = shape?.stringArgs.has(key) === true;
|
|
68
|
-
const rendered = isString && typeof value === "string" ? value : stringifyJson(value);
|
|
69
|
-
body += `<parameter name="${escapeXmlAttr(key)}">${rendered}</parameter>`;
|
|
70
|
-
}
|
|
71
|
-
return `${body}</invoke>`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function renderInvokes(calls: readonly ToolCall[], tools: NonNullable<DialectRenderOptions["tools"]>): string {
|
|
75
|
-
const shapes = buildArgShapes(tools);
|
|
76
|
-
return calls.map(call => renderInvoke(call, shapes.get(call.name))).join("\n");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
63
|
const definition: DialectDefinition = {
|
|
80
64
|
dialect: "xml",
|
|
81
65
|
prompt: dialectPrompt,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Api, type Model, modelKind } from "@oh-my-pi/pi-catalog/types";
|
|
2
2
|
import {
|
|
3
3
|
applyCodexResidencyHeader,
|
|
4
4
|
CODEX_BASE_URL,
|
|
@@ -155,7 +155,10 @@ export async function generateHostedImage(
|
|
|
155
155
|
action: content.length > 1 ? "edit" : "generate",
|
|
156
156
|
output_format: "webp",
|
|
157
157
|
...(size ? { size } : {}),
|
|
158
|
-
|
|
158
|
+
// A chat model generating on its own lets the host pick the image model.
|
|
159
|
+
...(model.api === "openai-responses" && modelKind(model) === "image"
|
|
160
|
+
? { model: model.requestModelId ?? model.id }
|
|
161
|
+
: {}),
|
|
159
162
|
};
|
|
160
163
|
const body = {
|
|
161
164
|
model: carrier.requestModelId ?? carrier.id,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Model } from "@oh-my-pi/pi-catalog/types";
|
|
2
2
|
import * as AIError from "../error";
|
|
3
|
+
import { resolveXaiBaseUrl } from "../providers/xai-base-url";
|
|
3
4
|
import {
|
|
4
5
|
decodeImageResponse,
|
|
5
6
|
imageBaseUrl,
|
|
@@ -54,11 +55,16 @@ export async function generateOpenAIImage(
|
|
|
54
55
|
: { ...generationBody, images: references }
|
|
55
56
|
: { ...generationBody, input_references: references };
|
|
56
57
|
const baseUrl = imageBaseUrl(model);
|
|
58
|
+
// xAI resolves the endpoint per bearer: XAI_BASE_URL never receives an xai-oauth OAuth access token.
|
|
59
|
+
const endpoint = (path: string) =>
|
|
60
|
+
isXAI
|
|
61
|
+
? (bearer: string) => `${resolveXaiBaseUrl(model.provider, baseUrl, bearer) ?? baseUrl}${path}`
|
|
62
|
+
: `${baseUrl}${path}`;
|
|
57
63
|
let response: unknown;
|
|
58
64
|
if (references.length === 0) {
|
|
59
65
|
response = await postJson({
|
|
60
66
|
model,
|
|
61
|
-
url:
|
|
67
|
+
url: endpoint("/images/generations"),
|
|
62
68
|
body: generationBody,
|
|
63
69
|
apiKey: options.apiKey,
|
|
64
70
|
fetch: fetchImpl,
|
|
@@ -78,7 +84,7 @@ export async function generateOpenAIImage(
|
|
|
78
84
|
}
|
|
79
85
|
response = await postMultipart({
|
|
80
86
|
model,
|
|
81
|
-
url:
|
|
87
|
+
url: endpoint("/images/edits"),
|
|
82
88
|
body: form,
|
|
83
89
|
apiKey: options.apiKey,
|
|
84
90
|
fetch: fetchImpl,
|
|
@@ -87,7 +93,7 @@ export async function generateOpenAIImage(
|
|
|
87
93
|
} else {
|
|
88
94
|
response = await postJson({
|
|
89
95
|
model,
|
|
90
|
-
url:
|
|
96
|
+
url: endpoint("/images/edits"),
|
|
91
97
|
body,
|
|
92
98
|
apiKey: options.apiKey,
|
|
93
99
|
fetch: fetchImpl,
|
|
@@ -98,7 +104,7 @@ export async function generateOpenAIImage(
|
|
|
98
104
|
if (!(error instanceof AIError.ProviderHttpError) || error.status !== 404) throw error;
|
|
99
105
|
response = await postJson({
|
|
100
106
|
model,
|
|
101
|
-
url:
|
|
107
|
+
url: endpoint("/images/generations"),
|
|
102
108
|
body,
|
|
103
109
|
apiKey: options.apiKey,
|
|
104
110
|
fetch: fetchImpl,
|
package/src/images/shared.ts
CHANGED
|
@@ -69,9 +69,12 @@ async function parseImageApiResponse(model: Model, response: Response): Promise<
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/** Request URL, or a builder for routes that depend on the bearer (xAI's `XAI_BASE_URL` rule). */
|
|
73
|
+
type ImageRequestUrl = string | ((bearer: string) => string);
|
|
74
|
+
|
|
72
75
|
export async function postJson(options: {
|
|
73
76
|
model: Model;
|
|
74
|
-
url:
|
|
77
|
+
url: ImageRequestUrl;
|
|
75
78
|
body: unknown;
|
|
76
79
|
apiKey: ApiKey;
|
|
77
80
|
fetch: FetchImpl;
|
|
@@ -80,7 +83,8 @@ export async function postJson(options: {
|
|
|
80
83
|
return withAuth(
|
|
81
84
|
options.apiKey,
|
|
82
85
|
async key => {
|
|
83
|
-
const
|
|
86
|
+
const url = typeof options.url === "string" ? options.url : options.url(key);
|
|
87
|
+
const response = await options.fetch(url, {
|
|
84
88
|
method: "POST",
|
|
85
89
|
headers: {
|
|
86
90
|
...(await modelHeaders(options.model, options.signal)),
|
|
@@ -99,7 +103,7 @@ export async function postJson(options: {
|
|
|
99
103
|
|
|
100
104
|
export async function postMultipart(options: {
|
|
101
105
|
model: Model;
|
|
102
|
-
url:
|
|
106
|
+
url: ImageRequestUrl;
|
|
103
107
|
body: FormData;
|
|
104
108
|
apiKey: ApiKey;
|
|
105
109
|
fetch: FetchImpl;
|
|
@@ -108,7 +112,8 @@ export async function postMultipart(options: {
|
|
|
108
112
|
return withAuth(
|
|
109
113
|
options.apiKey,
|
|
110
114
|
async key => {
|
|
111
|
-
const
|
|
115
|
+
const url = typeof options.url === "string" ? options.url : options.url(key);
|
|
116
|
+
const response = await options.fetch(url, {
|
|
112
117
|
method: "POST",
|
|
113
118
|
headers: {
|
|
114
119
|
...(await modelHeaders(options.model, options.signal)),
|
|
@@ -56,6 +56,7 @@ import { invalidateAwsCredentialCache, resolveAwsCredentials } from "./aws-crede
|
|
|
56
56
|
import { decodeEventStream } from "./aws-eventstream";
|
|
57
57
|
import { signRequest } from "./aws-sigv4";
|
|
58
58
|
import { parseAnthropicInputTransformations, THINKING_BINDING_CONTROLS_BETA } from "./anthropic-wire";
|
|
59
|
+
import { isBedrockRequestMetadataValue } from "./bedrock-request-metadata";
|
|
59
60
|
import { transformMessages } from "./transform-messages";
|
|
60
61
|
|
|
61
62
|
/**
|
|
@@ -381,9 +382,7 @@ interface MetadataEvent {
|
|
|
381
382
|
};
|
|
382
383
|
}
|
|
383
384
|
|
|
384
|
-
const REQUEST_METADATA_PATTERN = /^[a-zA-Z0-9\s:_@$#=/+,\-.]*$/;
|
|
385
385
|
const REQUEST_METADATA_MAX_ENTRIES = 16;
|
|
386
|
-
const REQUEST_METADATA_MAX_LENGTH = 256;
|
|
387
386
|
|
|
388
387
|
/**
|
|
389
388
|
* Bedrock rejects the whole invocation on a malformed `requestMetadata` entry.
|
|
@@ -400,10 +399,8 @@ function sanitizeRequestMetadata(raw: unknown): Record<string, string> | undefin
|
|
|
400
399
|
if (
|
|
401
400
|
typeof value !== "string" ||
|
|
402
401
|
key.length < 1 ||
|
|
403
|
-
key
|
|
404
|
-
!
|
|
405
|
-
value.length > REQUEST_METADATA_MAX_LENGTH ||
|
|
406
|
-
!REQUEST_METADATA_PATTERN.test(value) ||
|
|
402
|
+
!isBedrockRequestMetadataValue(key) ||
|
|
403
|
+
!isBedrockRequestMetadataValue(value) ||
|
|
407
404
|
kept >= REQUEST_METADATA_MAX_ENTRIES
|
|
408
405
|
) {
|
|
409
406
|
dropped.push(key);
|
|
@@ -446,6 +443,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
446
443
|
};
|
|
447
444
|
|
|
448
445
|
const blocks = output.content as Block[];
|
|
446
|
+
const contentIndexByBlockIndex = new Map<number, number>();
|
|
449
447
|
let rawRequestDump: RawHttpRequestDump | undefined;
|
|
450
448
|
const region = resolveBedrockRegion(model.id, options);
|
|
451
449
|
|
|
@@ -640,7 +638,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
640
638
|
if (messageType === "exception") {
|
|
641
639
|
const exceptionType = message.headers[":exception-type"] || "Exception";
|
|
642
640
|
const payload = safeParsePayload(message.payload) as { message?: string } | undefined;
|
|
643
|
-
const errorMessage = payload?.message ||
|
|
641
|
+
const errorMessage = payload?.message || PAYLOAD_DECODER.decode(message.payload);
|
|
644
642
|
const text = `${exceptionType}: ${errorMessage}`;
|
|
645
643
|
throw new AIError.BedrockApiError(text, bedrockStreamExceptionStatus(exceptionType), {
|
|
646
644
|
code: exceptionType,
|
|
@@ -648,7 +646,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
648
646
|
}
|
|
649
647
|
if (messageType === "error") {
|
|
650
648
|
const code = message.headers[":error-code"] || "UnknownError";
|
|
651
|
-
const errorMessage = message.headers[":error-message"] ||
|
|
649
|
+
const errorMessage = message.headers[":error-message"] || PAYLOAD_DECODER.decode(message.payload);
|
|
652
650
|
throw new AIError.BedrockApiError(`${code}: ${errorMessage}`, bedrockStreamExceptionStatus(code), {
|
|
653
651
|
code,
|
|
654
652
|
});
|
|
@@ -673,16 +671,35 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
673
671
|
}
|
|
674
672
|
case "contentBlockStart": {
|
|
675
673
|
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
676
|
-
handleContentBlockStart(
|
|
674
|
+
handleContentBlockStart(
|
|
675
|
+
payload as ContentBlockStartEvent,
|
|
676
|
+
blocks,
|
|
677
|
+
contentIndexByBlockIndex,
|
|
678
|
+
output,
|
|
679
|
+
stream,
|
|
680
|
+
sentinelInjected,
|
|
681
|
+
);
|
|
677
682
|
break;
|
|
678
683
|
}
|
|
679
684
|
case "contentBlockDelta": {
|
|
680
685
|
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
681
|
-
handleContentBlockDelta(
|
|
686
|
+
handleContentBlockDelta(
|
|
687
|
+
payload as ContentBlockDeltaEvent,
|
|
688
|
+
blocks,
|
|
689
|
+
contentIndexByBlockIndex,
|
|
690
|
+
output,
|
|
691
|
+
stream,
|
|
692
|
+
);
|
|
682
693
|
break;
|
|
683
694
|
}
|
|
684
695
|
case "contentBlockStop": {
|
|
685
|
-
handleContentBlockStop(
|
|
696
|
+
handleContentBlockStop(
|
|
697
|
+
payload as ContentBlockStopEvent,
|
|
698
|
+
blocks,
|
|
699
|
+
contentIndexByBlockIndex,
|
|
700
|
+
output,
|
|
701
|
+
stream,
|
|
702
|
+
);
|
|
686
703
|
break;
|
|
687
704
|
}
|
|
688
705
|
case "messageStop": {
|
|
@@ -782,18 +799,40 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
782
799
|
return stream;
|
|
783
800
|
};
|
|
784
801
|
|
|
802
|
+
/** Shared across events: every payload decode is a complete, non-streaming call. */
|
|
803
|
+
const PAYLOAD_DECODER = new TextDecoder();
|
|
804
|
+
|
|
785
805
|
function safeParsePayload(payload: Uint8Array): unknown {
|
|
786
806
|
if (payload.length === 0) return {};
|
|
787
807
|
try {
|
|
788
|
-
return JSON.parse(
|
|
808
|
+
return JSON.parse(PAYLOAD_DECODER.decode(payload));
|
|
789
809
|
} catch {
|
|
790
810
|
return undefined;
|
|
791
811
|
}
|
|
792
812
|
}
|
|
793
813
|
|
|
814
|
+
/**
|
|
815
|
+
* Append a streamed block and index it by Bedrock's `contentBlockIndex`, so
|
|
816
|
+
* per-delta routing is an O(1) lookup instead of a scan over every block
|
|
817
|
+
* (quadratic over a long turn). The first block registered for an index wins,
|
|
818
|
+
* as the first-match scan did. Returns the block's content index.
|
|
819
|
+
*/
|
|
820
|
+
function pushStreamBlock(
|
|
821
|
+
blocks: Block[],
|
|
822
|
+
contentIndexByBlockIndex: Map<number, number>,
|
|
823
|
+
block: Block,
|
|
824
|
+
contentBlockIndex: number,
|
|
825
|
+
): number {
|
|
826
|
+
const contentIndex = blocks.length;
|
|
827
|
+
blocks.push(block);
|
|
828
|
+
if (!contentIndexByBlockIndex.has(contentBlockIndex)) contentIndexByBlockIndex.set(contentBlockIndex, contentIndex);
|
|
829
|
+
return contentIndex;
|
|
830
|
+
}
|
|
831
|
+
|
|
794
832
|
function handleContentBlockStart(
|
|
795
833
|
event: ContentBlockStartEvent,
|
|
796
834
|
blocks: Block[],
|
|
835
|
+
contentIndexByBlockIndex: Map<number, number>,
|
|
797
836
|
output: AssistantMessage,
|
|
798
837
|
stream: AssistantMessageEventStream,
|
|
799
838
|
sentinelInjected: boolean,
|
|
@@ -815,29 +854,29 @@ function handleContentBlockStart(
|
|
|
815
854
|
[kStreamingPartialJson]: "",
|
|
816
855
|
[kStreamingBlockIndex]: index,
|
|
817
856
|
};
|
|
818
|
-
|
|
819
|
-
stream.push({ type: "toolcall_start", contentIndex
|
|
857
|
+
const contentIndex = pushStreamBlock(blocks, contentIndexByBlockIndex, block, index);
|
|
858
|
+
stream.push({ type: "toolcall_start", contentIndex, partial: output });
|
|
820
859
|
}
|
|
821
860
|
}
|
|
822
861
|
|
|
823
862
|
function handleContentBlockDelta(
|
|
824
863
|
event: ContentBlockDeltaEvent,
|
|
825
864
|
blocks: Block[],
|
|
865
|
+
contentIndexByBlockIndex: Map<number, number>,
|
|
826
866
|
output: AssistantMessage,
|
|
827
867
|
stream: AssistantMessageEventStream,
|
|
828
868
|
): void {
|
|
829
869
|
const contentBlockIndex = event.contentBlockIndex;
|
|
830
870
|
const delta = event.delta;
|
|
831
|
-
let index =
|
|
871
|
+
let index = contentIndexByBlockIndex.get(contentBlockIndex) ?? -1;
|
|
832
872
|
let block = blocks[index];
|
|
833
873
|
|
|
834
874
|
if (delta?.text !== undefined) {
|
|
835
875
|
// If no text block exists yet, create one — `handleContentBlockStart` is not sent for text blocks
|
|
836
876
|
if (!block) {
|
|
837
877
|
const newBlock: Block = { type: "text", text: "", [kStreamingBlockIndex]: contentBlockIndex };
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
block = blocks[index];
|
|
878
|
+
index = pushStreamBlock(blocks, contentIndexByBlockIndex, newBlock, contentBlockIndex);
|
|
879
|
+
block = newBlock;
|
|
841
880
|
stream.push({ type: "text_start", contentIndex: index, partial: output });
|
|
842
881
|
}
|
|
843
882
|
if (block.type === "text") {
|
|
@@ -863,9 +902,8 @@ function handleContentBlockDelta(
|
|
|
863
902
|
thinkingSignature: "",
|
|
864
903
|
[kStreamingBlockIndex]: contentBlockIndex,
|
|
865
904
|
};
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
thinkingBlock = blocks[thinkingIndex];
|
|
905
|
+
thinkingIndex = pushStreamBlock(blocks, contentIndexByBlockIndex, newBlock, contentBlockIndex);
|
|
906
|
+
thinkingBlock = newBlock;
|
|
869
907
|
stream.push({ type: "thinking_start", contentIndex: thinkingIndex, partial: output });
|
|
870
908
|
}
|
|
871
909
|
|
|
@@ -901,10 +939,11 @@ function handleMetadata(event: MetadataEvent, model: Model<"bedrock-converse-str
|
|
|
901
939
|
function handleContentBlockStop(
|
|
902
940
|
event: ContentBlockStopEvent,
|
|
903
941
|
blocks: Block[],
|
|
942
|
+
contentIndexByBlockIndex: Map<number, number>,
|
|
904
943
|
output: AssistantMessage,
|
|
905
944
|
stream: AssistantMessageEventStream,
|
|
906
945
|
): void {
|
|
907
|
-
const index =
|
|
946
|
+
const index = contentIndexByBlockIndex.get(event.contentBlockIndex) ?? -1;
|
|
908
947
|
const block = blocks[index];
|
|
909
948
|
if (!block) return;
|
|
910
949
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isOfficialAnthropicApiUrl } from "@oh-my-pi/pi-catalog/compat/anthropic";
|
|
1
|
+
import { isBedrockAnthropicRoute, isOfficialAnthropicApiUrl } from "@oh-my-pi/pi-catalog/compat/anthropic";
|
|
2
2
|
import type { Model } from "../types";
|
|
3
3
|
import type { AnthropicMessagesClientLike } from "./anthropic-client";
|
|
4
4
|
import { normalizeAnthropicBaseUrl, resolveDirectAnthropicBaseUrl } from "./anthropic-state";
|
|
@@ -46,6 +46,15 @@ export function supportsAnthropicCompaction(model: Model<"anthropic-messages">,
|
|
|
46
46
|
(model.provider === "anthropic"
|
|
47
47
|
? resolveDirectAnthropicBaseUrl(model)
|
|
48
48
|
: normalizeAnthropicBaseUrl(model.baseUrl));
|
|
49
|
+
// Bedrock's Anthropic Messages API implements on-demand compaction. The flag is detected
|
|
50
|
+
// from a Bedrock `/anthropic` baseUrl, or set in models.yml for a proxy or a reroute; it
|
|
51
|
+
// applies to the model's own endpoint or a Bedrock `/anthropic` route it reaches.
|
|
52
|
+
if (
|
|
53
|
+
model.compat.bedrockMessagesApi === true &&
|
|
54
|
+
(isBedrockAnthropicRoute(route) || route === normalizeAnthropicBaseUrl(model.baseUrl))
|
|
55
|
+
) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
49
58
|
return (
|
|
50
59
|
isSupportedCompactionEndpoint(route) &&
|
|
51
60
|
(model.compat.firstPartyProvider === true ||
|
|
@@ -272,7 +272,18 @@ export type ThinkingConfigAdaptive = {
|
|
|
272
272
|
block_binding?: ThinkingBlockBinding;
|
|
273
273
|
};
|
|
274
274
|
|
|
275
|
-
|
|
275
|
+
/**
|
|
276
|
+
* Sonnet 5.5's replacement for `disabled`: no up-front thinking, progress
|
|
277
|
+
* updates between tool calls only. Takes no other field, and effort above
|
|
278
|
+
* `high` is rejected alongside it.
|
|
279
|
+
*/
|
|
280
|
+
export type ThinkingConfigBetweenTools = { type: "between_tools" };
|
|
281
|
+
|
|
282
|
+
export type ThinkingConfigParam =
|
|
283
|
+
| ThinkingConfigEnabled
|
|
284
|
+
| ThinkingConfigDisabled
|
|
285
|
+
| ThinkingConfigAdaptive
|
|
286
|
+
| ThinkingConfigBetweenTools;
|
|
276
287
|
|
|
277
288
|
export type OutputConfig = {
|
|
278
289
|
/** Adaptive-thinking effort level (effort beta). */
|