@oh-my-pi/pi-ai 16.0.9 → 16.0.10
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 +16 -0
- package/dist/types/registry/oauth/types.d.ts +6 -0
- package/package.json +3 -3
- package/src/dialect/owned-stream.ts +1 -1
- package/src/dialect/pi.md +31 -33
- package/src/dialect/pi.ts +339 -452
- package/src/providers/amazon-bedrock.ts +13 -1
- package/src/registry/oauth/index.ts +2 -0
- package/src/registry/oauth/types.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.0.10] - 2026-06-18
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Replaced the old legacy XML-ish `pi` owned tool-calling dialect with the new sigil-delimited format (`§` call header with inline `key=value` scalars, `«…»` verbatim body fence for the dominant string argument, `¤` reasoning, `‡‡` tool result) using single-token markers that never occur in source code. Verbatim fences escalate Markdown-style (`««…»»`) so re-rendered history never collides with payload content, and the scanner gates a bare `§` on an exact known-tool name to avoid swallowing prose. Round-trips and streams through the existing scanner contract at ~46% fewer tokens than the legacy format on typical calls; selectable via `tools.format` or `PI_DIALECT=pi`.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Updated `pi` dialect formatting to use a token-frugal, sigil-delimited format (`§`, `¤`, `‡‡`)
|
|
14
|
+
- Updated `pi` dialect body fences to automatically escalate when content contains fence markers
|
|
15
|
+
- Changed `pi` dialect tool results response format to `‡‡` blocks
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Fixed Bedrock application inference profile ARNs to route requests to the ARN's region instead of the default Bedrock runtime region. ([#3004](https://github.com/can1357/oh-my-pi/issues/3004))
|
|
20
|
+
|
|
5
21
|
## [16.0.9] - 2026-06-18
|
|
6
22
|
|
|
7
23
|
### Fixed
|
|
@@ -25,6 +25,12 @@ export interface OAuthProviderInfo {
|
|
|
25
25
|
id: OAuthProviderId;
|
|
26
26
|
name: string;
|
|
27
27
|
available: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Provider id the login stores credentials under, when it differs from `id`
|
|
30
|
+
* (e.g. `openai-codex-device` ⇒ `openai-codex`). Lets callers map a login
|
|
31
|
+
* entry back to the model provider it authenticates.
|
|
32
|
+
*/
|
|
33
|
+
storeCredentialsAs?: string;
|
|
28
34
|
}
|
|
29
35
|
export interface OAuthController {
|
|
30
36
|
onAuth?(info: OAuthAuthInfo): void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "16.0.
|
|
4
|
+
"version": "16.0.10",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.0",
|
|
41
|
-
"@oh-my-pi/pi-catalog": "16.0.
|
|
42
|
-
"@oh-my-pi/pi-utils": "16.0.
|
|
41
|
+
"@oh-my-pi/pi-catalog": "16.0.10",
|
|
42
|
+
"@oh-my-pi/pi-utils": "16.0.10",
|
|
43
43
|
"arktype": "^2.2.0",
|
|
44
44
|
"partial-json": "^0.1.7",
|
|
45
45
|
"zod": "^4"
|
|
@@ -19,7 +19,7 @@ const RESPONSE_OPEN_TOKENS: Record<Dialect, readonly string[]> = {
|
|
|
19
19
|
minimax: ["<function_results>", "<tool_response>"],
|
|
20
20
|
deepseek: ["<|tool▁outputs▁begin|>", "<|tool▁output▁begin|>"],
|
|
21
21
|
harmony: ["<|start|>functions."],
|
|
22
|
-
pi: ["
|
|
22
|
+
pi: ["‡‡"],
|
|
23
23
|
qwen3: ["<tool_response>"],
|
|
24
24
|
gemini: ["```tool_outputs"],
|
|
25
25
|
gemma: ["<|tool_response>"],
|
package/src/dialect/pi.md
CHANGED
|
@@ -1,57 +1,55 @@
|
|
|
1
1
|
## Format guide
|
|
2
2
|
|
|
3
|
-
A tool call
|
|
3
|
+
A tool call begins with `§` immediately followed by the function NAME (start each call on its own line). Scalar arguments follow on the same line as `key=value` pairs; a single large or multi-line string argument goes in a verbatim body fenced by `«…»` right after the header.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
<call:read path="src/a.ts" offset=50/>
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
Objects and arrays use child elements, repeating an element for each array item:
|
|
5
|
+
Scalar-only call (the line ends the call):
|
|
10
6
|
|
|
11
7
|
```text
|
|
12
|
-
|
|
13
|
-
<object>
|
|
14
|
-
<y>4</y>
|
|
15
|
-
<list>alpha</list>
|
|
16
|
-
<list>beta</list>
|
|
17
|
-
</object>
|
|
18
|
-
</call:configure>
|
|
8
|
+
§read path=src/a.ts offset=50 limit=200
|
|
19
9
|
```
|
|
20
10
|
|
|
21
|
-
|
|
11
|
+
Call with a verbatim body — everything between `«` and `»` is taken literally, no quoting or escaping:
|
|
22
12
|
|
|
23
13
|
```text
|
|
24
|
-
|
|
14
|
+
§edit path=src/server/auth.ts«
|
|
25
15
|
*** Begin Patch
|
|
26
|
-
|
|
16
|
+
*** Update File: src/server/auth.ts
|
|
17
|
+
@@ class AuthService
|
|
18
|
+
- login(user) {
|
|
19
|
+
+ async login(user, opts) {
|
|
27
20
|
*** End Patch
|
|
28
|
-
|
|
21
|
+
»
|
|
29
22
|
```
|
|
30
23
|
|
|
31
|
-
|
|
24
|
+
Argument values:
|
|
25
|
+
|
|
26
|
+
- Strings are written bare and verbatim (`path=src/a.ts`). Quote with `"…"` only when the value contains spaces or starts with `"`, `[`, or `{` (`_i="run the tests"`).
|
|
27
|
+
- Numbers, booleans, and `null` are JSON literals (`offset=50`, `force=true`).
|
|
28
|
+
- Arrays and objects are inline JSON (`paths=["src","test"]`).
|
|
29
|
+
- The body fence holds the call's first long/multi-line string parameter; its key is implied, never written.
|
|
30
|
+
|
|
31
|
+
Private reasoning goes in a `¤…¤` block before your calls:
|
|
32
32
|
|
|
33
33
|
```text
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
¤
|
|
35
|
+
brief reasoning
|
|
36
|
+
¤
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
Tool results arrive in `‡‡…‡‡` blocks, read in call order:
|
|
40
40
|
|
|
41
41
|
```text
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
‡‡
|
|
43
|
+
verbatim tool result
|
|
44
|
+
‡‡
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
## Rules
|
|
48
48
|
|
|
49
|
-
- `NAME`
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
- Private reasoning goes in a `<thinking>…</thinking>` block before your calls; NEVER put calls inside it.
|
|
56
|
-
- Read each `<tool_response>` in call order. NEVER emit `<tool_response>` yourself.
|
|
49
|
+
- `NAME` MUST match a listed function; never wrap calls in JSON or fences.
|
|
50
|
+
- Put each scalar argument once as `key=value`; reserve the `«…»` body for the one dominant string argument (file contents, patches, commands, queries).
|
|
51
|
+
- Body text is verbatim — include no surrounding quotes. If the body itself contains `»`, widen BOTH guillemet fences equally (`««…»»`, `«««…»»»`).
|
|
52
|
+
- Emit parallel calls as consecutive `§…` blocks. NEVER invent call ids; results are positional.
|
|
53
|
+
- Private reasoning goes in a `¤…¤` block before your calls; NEVER put calls inside it, and keep a literal `¤` out of the reasoning text.
|
|
54
|
+
- Read each `‡‡…‡‡` result in call order. NEVER emit a `‡‡` block yourself.
|
|
57
55
|
- After emitting your tool calls, YOU MUST EMIT THE STOP SEQUENCE AND HALT.
|
package/src/dialect/pi.ts
CHANGED
|
@@ -1,19 +1,8 @@
|
|
|
1
1
|
import type { Message, ToolCall } from "../types";
|
|
2
2
|
import type { ToolArgShape } from "./coercion";
|
|
3
|
-
import {
|
|
4
|
-
buildArgShapes,
|
|
5
|
-
coerceValue,
|
|
6
|
-
collectSchemaTypes,
|
|
7
|
-
getArrayItemSchema,
|
|
8
|
-
getObjectProperties,
|
|
9
|
-
isArraySchema,
|
|
10
|
-
isObjectSchema,
|
|
11
|
-
isStringOnlySchema,
|
|
12
|
-
mintToolCallId,
|
|
13
|
-
partialSuffixOverlapAny,
|
|
14
|
-
} from "./coercion";
|
|
3
|
+
import { buildArgShapes, coerceValue, isStringOnlySchema, mintToolCallId, partialSuffixOverlapAny } from "./coercion";
|
|
15
4
|
import dialectPrompt from "./pi.md" with { type: "text" };
|
|
16
|
-
import { renderChatMlTranscript,
|
|
5
|
+
import { renderChatMlTranscript, stringifyJson } from "./rendering";
|
|
17
6
|
import type {
|
|
18
7
|
DialectDefinition,
|
|
19
8
|
DialectRenderOptions,
|
|
@@ -23,57 +12,66 @@ import type {
|
|
|
23
12
|
InbandScannerOptions,
|
|
24
13
|
} from "./types";
|
|
25
14
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
15
|
+
// Pi — a sigil-delimited, token-frugal owned dialect.
|
|
16
|
+
//
|
|
17
|
+
// §read path=src/a.ts offset=50 ← scalar-only call (newline-terminated)
|
|
18
|
+
// §edit path=src/a.ts« ← header + verbatim body fence
|
|
19
|
+
// *** Begin Patch
|
|
20
|
+
// ...
|
|
21
|
+
// *** End Patch»
|
|
22
|
+
//
|
|
23
|
+
// Design goals vs the XML-ish `pi` dialect:
|
|
24
|
+
// - one-token structural sigils (`§` call, `«»` body fence, `¤` thinking, `‡‡`
|
|
25
|
+
// tool result — each a single o200k token that never occurs in source code)
|
|
26
|
+
// instead of `<call:NAME>` / `</call:NAME>` (3 tokens + the repeated name);
|
|
27
|
+
// - scalar arguments inline as `key=value` (the key appears once) rather than
|
|
28
|
+
// `<key>value</key>` (key twice + four bracket tokens);
|
|
29
|
+
// - the dominant string argument fills a verbatim body fence, dropping its key
|
|
30
|
+
// entirely and needing no escaping for code/patches.
|
|
31
|
+
//
|
|
32
|
+
// Verbatim fences (body `«»`, result `‡‡`) escalate Markdown/raw-string style:
|
|
33
|
+
// when the content contains the closer, the renderer widens the fence (`««…»»`,
|
|
34
|
+
// `‡‡‡…‡‡‡`) so re-rendered history can never collide with payload content.
|
|
35
|
+
|
|
36
|
+
const CALL_SIGIL = "§";
|
|
37
|
+
const FENCE_OPEN = "«";
|
|
38
|
+
const FENCE_CLOSE = "»";
|
|
39
|
+
const THINK_SIGIL = "¤";
|
|
40
|
+
const RESULT_FENCE = "‡";
|
|
41
|
+
const OUTSIDE_TAGS = [CALL_SIGIL, THINK_SIGIL] as const;
|
|
42
|
+
const CALL_TAGS = [CALL_SIGIL] as const;
|
|
31
43
|
const NAME_START = /[A-Za-z_]/;
|
|
32
44
|
const NAME_CHAR = /[A-Za-z0-9_-]/;
|
|
33
45
|
const EMPTY_STRING_ARGS: ReadonlySet<string> = new Set<string>();
|
|
34
46
|
|
|
35
47
|
type ScannerState = "outside" | "body" | "thinking";
|
|
36
|
-
type BodyMode = "undecided" | "inline" | "members";
|
|
37
48
|
|
|
38
|
-
type
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
selfClosing: boolean;
|
|
44
|
-
end: number;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
type MembersResult = {
|
|
48
|
-
ok: boolean;
|
|
49
|
-
value: Record<string, unknown>;
|
|
50
|
-
next: number;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
type ValueResult = {
|
|
54
|
-
ok: boolean;
|
|
55
|
-
value: unknown;
|
|
56
|
-
next: number;
|
|
57
|
-
};
|
|
49
|
+
type HeaderEnd =
|
|
50
|
+
| { kind: "fence"; index: number }
|
|
51
|
+
| { kind: "newline"; index: number }
|
|
52
|
+
| { kind: "eof"; index: number }
|
|
53
|
+
| { kind: "incomplete" };
|
|
58
54
|
|
|
59
55
|
export class PiNativeInbandScanner implements InbandScanner {
|
|
60
56
|
#buffer = "";
|
|
61
57
|
#state: ScannerState = "outside";
|
|
62
|
-
#bodyMode: BodyMode = "undecided";
|
|
63
58
|
#id = "";
|
|
64
59
|
#name = "";
|
|
65
60
|
#args: Record<string, unknown> = {};
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
61
|
+
#bodyKey = "";
|
|
62
|
+
#bodyValue = "";
|
|
63
|
+
#bodyLeading = false;
|
|
64
|
+
#closeMarker = "";
|
|
69
65
|
#rawBlock = "";
|
|
66
|
+
#thinking = "";
|
|
70
67
|
readonly #argShapes: Map<string, ToolArgShape>;
|
|
71
68
|
readonly #stringArgs: (toolName: string) => ReadonlySet<string>;
|
|
72
|
-
#
|
|
69
|
+
readonly #knowsTools: boolean;
|
|
73
70
|
readonly #parseThinking: boolean;
|
|
74
71
|
|
|
75
72
|
constructor(options: InbandScannerOptions = {}) {
|
|
76
73
|
this.#argShapes = buildArgShapes(options.tools);
|
|
74
|
+
this.#knowsTools = this.#argShapes.size > 0;
|
|
77
75
|
this.#stringArgs =
|
|
78
76
|
options.stringArgs ?? (toolName => this.#argShapes.get(toolName)?.stringArgs ?? EMPTY_STRING_ARGS);
|
|
79
77
|
this.#parseThinking = options.parseThinking !== false;
|
|
@@ -96,37 +94,19 @@ export class PiNativeInbandScanner implements InbandScanner {
|
|
|
96
94
|
if (!this.#consumeOutside(events, final)) break;
|
|
97
95
|
continue;
|
|
98
96
|
}
|
|
99
|
-
|
|
100
97
|
if (this.#state === "thinking") {
|
|
101
98
|
if (!this.#consumeThinking(events, final)) break;
|
|
102
99
|
continue;
|
|
103
100
|
}
|
|
104
|
-
|
|
105
|
-
if (this.#bodyMode === "undecided") {
|
|
106
|
-
const mode = this.#classifyBody(final);
|
|
107
|
-
if (!mode) break;
|
|
108
|
-
this.#bodyMode = mode;
|
|
109
|
-
if (mode === "inline") {
|
|
110
|
-
this.#inlineKey = this.#inlineTargetKey() ?? "input";
|
|
111
|
-
this.#inlineValue = "";
|
|
112
|
-
this.#inlineLeading = true;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (this.#bodyMode === "inline") {
|
|
117
|
-
if (!this.#consumeInline(events, final)) break;
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (!this.#consumeMembers(events, final)) break;
|
|
101
|
+
if (!this.#consumeBody(events, final)) break;
|
|
122
102
|
}
|
|
123
103
|
if (final && this.#state === "thinking") this.#endThinking(events);
|
|
124
104
|
return events;
|
|
125
105
|
}
|
|
126
106
|
|
|
127
107
|
#consumeOutside(events: InbandScanEvent[], final: boolean): boolean {
|
|
128
|
-
const call = this.#buffer.indexOf(
|
|
129
|
-
const think = this.#parseThinking ? this.#buffer.indexOf(
|
|
108
|
+
const call = this.#buffer.indexOf(CALL_SIGIL);
|
|
109
|
+
const think = this.#parseThinking ? this.#buffer.indexOf(THINK_SIGIL) : -1;
|
|
130
110
|
let start = call;
|
|
131
111
|
let isThink = false;
|
|
132
112
|
if (think !== -1 && (start === -1 || think < start)) {
|
|
@@ -148,124 +128,109 @@ export class PiNativeInbandScanner implements InbandScanner {
|
|
|
148
128
|
}
|
|
149
129
|
|
|
150
130
|
if (isThink) {
|
|
151
|
-
this.#buffer = this.#buffer.slice(
|
|
131
|
+
this.#buffer = this.#buffer.slice(THINK_SIGIL.length);
|
|
152
132
|
this.#thinking = "";
|
|
153
133
|
events.push({ type: "thinkingStart" });
|
|
154
134
|
this.#state = "thinking";
|
|
155
135
|
return true;
|
|
156
136
|
}
|
|
157
137
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
138
|
+
return this.#beginCall(events, final);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Buffer starts with `§`. Resolve the tool name, then the header terminator.
|
|
142
|
+
// Returns false to wait for more input (call still streaming in).
|
|
143
|
+
#beginCall(events: InbandScanEvent[], final: boolean): boolean {
|
|
144
|
+
const nameStart = CALL_SIGIL.length;
|
|
145
|
+
if (nameStart >= this.#buffer.length && !final) return false; // just `§` so far
|
|
146
|
+
let nameEnd = nameStart;
|
|
147
|
+
if (!isNameStart(this.#buffer[nameEnd])) return this.#rejectSigil(events);
|
|
148
|
+
nameEnd++;
|
|
149
|
+
while (nameEnd < this.#buffer.length && isNameChar(this.#buffer[nameEnd])) nameEnd++;
|
|
150
|
+
if (nameEnd >= this.#buffer.length && !final) return false; // name may continue
|
|
151
|
+
|
|
152
|
+
const name = this.#buffer.slice(CALL_SIGIL.length, nameEnd);
|
|
153
|
+
// Guard against `§` in prose: only claim a known tool when schemas exist.
|
|
154
|
+
if (this.#knowsTools && !this.#argShapes.has(name)) return this.#rejectSigil(events);
|
|
155
|
+
|
|
156
|
+
const header = findHeaderEnd(this.#buffer, nameEnd);
|
|
157
|
+
if (header.kind === "fence") {
|
|
158
|
+
let runEnd = header.index;
|
|
159
|
+
while (runEnd < this.#buffer.length && this.#buffer[runEnd] === FENCE_OPEN) runEnd++;
|
|
160
|
+
if (runEnd >= this.#buffer.length && !final) return false; // fence run may grow
|
|
161
|
+
return this.#startCall(events, name, nameEnd, header.index, "fence", runEnd - header.index);
|
|
162
162
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (!tag) {
|
|
166
|
-
events.push({ type: "text", text: this.#buffer[0] ?? "" });
|
|
167
|
-
this.#buffer = this.#buffer.slice(1);
|
|
168
|
-
return true;
|
|
163
|
+
if (header.kind === "newline") {
|
|
164
|
+
return this.#startCall(events, name, nameEnd, header.index, "newline", 0);
|
|
169
165
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
events.push({
|
|
175
|
-
type: "toolEnd",
|
|
176
|
-
id: this.#id,
|
|
177
|
-
name: this.#name,
|
|
178
|
-
arguments: this.#args,
|
|
179
|
-
rawBlock: this.#rawBlock,
|
|
180
|
-
});
|
|
181
|
-
this.#reset();
|
|
182
|
-
return true;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
this.#state = "body";
|
|
186
|
-
this.#bodyMode = "undecided";
|
|
187
|
-
return true;
|
|
166
|
+
// "eof"/"incomplete": the header may still be streaming in — only a scalar
|
|
167
|
+
// call with no trailing newline at true end-of-stream finalizes here.
|
|
168
|
+
if (!final) return false;
|
|
169
|
+
return this.#startCall(events, name, nameEnd, this.#buffer.length, "eof", 0);
|
|
188
170
|
}
|
|
189
171
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
this.#emitThinking(this.#buffer.slice(0, this.#buffer.length - hold), events);
|
|
195
|
-
this.#buffer = this.#buffer.slice(this.#buffer.length - hold);
|
|
196
|
-
if (final) {
|
|
197
|
-
this.#endThinking(events);
|
|
198
|
-
this.#state = "outside";
|
|
199
|
-
}
|
|
200
|
-
return false;
|
|
201
|
-
}
|
|
202
|
-
this.#emitThinking(this.#buffer.slice(0, close), events);
|
|
203
|
-
this.#buffer = this.#buffer.slice(close + THINK_CLOSE.length);
|
|
204
|
-
this.#endThinking(events);
|
|
205
|
-
this.#state = "outside";
|
|
172
|
+
// `§` not followed by a known tool name is prose — surface it as literal text.
|
|
173
|
+
#rejectSigil(events: InbandScanEvent[]): boolean {
|
|
174
|
+
events.push({ type: "text", text: CALL_SIGIL });
|
|
175
|
+
this.#buffer = this.#buffer.slice(CALL_SIGIL.length);
|
|
206
176
|
return true;
|
|
207
177
|
}
|
|
208
178
|
|
|
209
|
-
#
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
this.#state = "outside";
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
#beginCall(tag: OpenTag, events: InbandScanEvent[]): void {
|
|
179
|
+
#startCall(
|
|
180
|
+
events: InbandScanEvent[],
|
|
181
|
+
name: string,
|
|
182
|
+
argsStart: number,
|
|
183
|
+
headerEnd: number,
|
|
184
|
+
kind: "fence" | "newline" | "eof",
|
|
185
|
+
fenceLen: number,
|
|
186
|
+
): boolean {
|
|
187
|
+
const shape = this.#argShapes.get(name);
|
|
222
188
|
this.#id = mintToolCallId();
|
|
223
|
-
this.#name =
|
|
224
|
-
this.#args =
|
|
225
|
-
this.#inlineKey = "";
|
|
226
|
-
this.#inlineValue = "";
|
|
227
|
-
this.#inlineLeading = false;
|
|
228
|
-
this.#rawBlock = this.#buffer.slice(0, tag.end);
|
|
189
|
+
this.#name = name;
|
|
190
|
+
this.#args = parseHeaderArgs(this.#buffer.slice(argsStart, headerEnd), shape?.properties ?? {});
|
|
229
191
|
events.push({ type: "toolStart", id: this.#id, name: this.#name });
|
|
230
|
-
}
|
|
231
192
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (this.#buffer[first] !== "<") return inlineKey ? "inline" : "members";
|
|
245
|
-
if (this.#buffer.startsWith(closeTag, first)) return inlineKey ? "inline" : "members";
|
|
246
|
-
if (this.#buffer.startsWith("</", first)) return "members";
|
|
247
|
-
|
|
248
|
-
const elementName = readElementNamePrefix(this.#buffer, first);
|
|
249
|
-
if (elementName === undefined) return final ? (inlineKey ? "inline" : "members") : undefined;
|
|
250
|
-
if (elementName.length === 0) return inlineKey ? "inline" : "members";
|
|
193
|
+
if (kind === "fence") {
|
|
194
|
+
const fenceEnd = headerEnd + fenceLen;
|
|
195
|
+
this.#rawBlock = this.#buffer.slice(0, fenceEnd);
|
|
196
|
+
this.#closeMarker = FENCE_CLOSE.repeat(fenceLen);
|
|
197
|
+
this.#bodyKey = this.#inlineTargetKey() ?? "input";
|
|
198
|
+
this.#bodyValue = "";
|
|
199
|
+
this.#bodyLeading = true;
|
|
200
|
+
this.#buffer = this.#buffer.slice(fenceEnd);
|
|
201
|
+
this.#state = "body";
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
251
204
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
205
|
+
this.#rawBlock = this.#buffer.slice(0, headerEnd);
|
|
206
|
+
events.push({
|
|
207
|
+
type: "toolEnd",
|
|
208
|
+
id: this.#id,
|
|
209
|
+
name: this.#name,
|
|
210
|
+
arguments: this.#args,
|
|
211
|
+
rawBlock: this.#rawBlock,
|
|
212
|
+
});
|
|
213
|
+
let next = headerEnd;
|
|
214
|
+
if (kind === "newline") {
|
|
215
|
+
if (this.#buffer[next] === "\r") next++;
|
|
216
|
+
if (this.#buffer[next] === "\n") next++;
|
|
217
|
+
}
|
|
218
|
+
this.#buffer = this.#buffer.slice(next);
|
|
219
|
+
this.#reset();
|
|
220
|
+
return true;
|
|
255
221
|
}
|
|
256
222
|
|
|
257
|
-
#
|
|
258
|
-
this.#
|
|
259
|
-
const
|
|
260
|
-
const close = this.#buffer.indexOf(closeTag);
|
|
223
|
+
#consumeBody(events: InbandScanEvent[], final: boolean): boolean {
|
|
224
|
+
this.#stripBodyLeading(final);
|
|
225
|
+
const close = this.#buffer.indexOf(this.#closeMarker);
|
|
261
226
|
if (close === -1) {
|
|
262
227
|
if (final) {
|
|
263
228
|
this.#reset();
|
|
264
229
|
this.#buffer = "";
|
|
265
230
|
return false;
|
|
266
231
|
}
|
|
267
|
-
const overlap = partialSuffixOverlapAny(this.#buffer, [
|
|
268
|
-
let hold = Math.max(
|
|
232
|
+
const overlap = partialSuffixOverlapAny(this.#buffer, [this.#closeMarker]);
|
|
233
|
+
let hold = Math.max(this.#closeMarker.length, overlap);
|
|
269
234
|
if (overlap > 0) {
|
|
270
235
|
const beforeOverlap = this.#buffer.length - overlap - 1;
|
|
271
236
|
if (this.#buffer[beforeOverlap] === "\n") {
|
|
@@ -277,67 +242,33 @@ export class PiNativeInbandScanner implements InbandScanner {
|
|
|
277
242
|
if (emitLength > 0) {
|
|
278
243
|
const delta = this.#buffer.slice(0, emitLength);
|
|
279
244
|
this.#rawBlock += delta;
|
|
280
|
-
this.#
|
|
245
|
+
this.#emitBodyDelta(delta, events);
|
|
281
246
|
this.#buffer = this.#buffer.slice(emitLength);
|
|
282
247
|
}
|
|
283
248
|
return false;
|
|
284
249
|
}
|
|
285
250
|
|
|
286
251
|
const rawDelta = this.#buffer.slice(0, close);
|
|
287
|
-
this.#rawBlock += rawDelta +
|
|
252
|
+
this.#rawBlock += rawDelta + this.#closeMarker;
|
|
288
253
|
let delta = rawDelta;
|
|
289
254
|
if (delta.endsWith("\r\n")) delta = delta.slice(0, -2);
|
|
290
255
|
else if (delta.endsWith("\n")) delta = delta.slice(0, -1);
|
|
291
|
-
this.#
|
|
292
|
-
this.#args[this.#
|
|
293
|
-
events.push({
|
|
294
|
-
|
|
256
|
+
this.#emitBodyDelta(delta, events);
|
|
257
|
+
this.#args[this.#bodyKey] = this.#bodyValue;
|
|
258
|
+
events.push({
|
|
259
|
+
type: "toolEnd",
|
|
260
|
+
id: this.#id,
|
|
261
|
+
name: this.#name,
|
|
262
|
+
arguments: this.#args,
|
|
263
|
+
rawBlock: this.#rawBlock,
|
|
264
|
+
});
|
|
265
|
+
this.#buffer = this.#buffer.slice(close + this.#closeMarker.length);
|
|
295
266
|
this.#reset();
|
|
296
267
|
return true;
|
|
297
268
|
}
|
|
298
269
|
|
|
299
|
-
#
|
|
300
|
-
|
|
301
|
-
let searchFrom = 0;
|
|
302
|
-
while (true) {
|
|
303
|
-
const close = this.#buffer.indexOf(closeTag, searchFrom);
|
|
304
|
-
if (close === -1) {
|
|
305
|
-
if (final) {
|
|
306
|
-
this.#reset();
|
|
307
|
-
this.#buffer = "";
|
|
308
|
-
}
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const body = this.#buffer.slice(0, close);
|
|
313
|
-
const parsed = parseMembers(body, 0, undefined, this.#shape()?.properties ?? {});
|
|
314
|
-
if (!parsed.ok || skipWhitespace(body, parsed.next) !== body.length) {
|
|
315
|
-
searchFrom = close + closeTag.length;
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const bodyArgs = parsed.value;
|
|
320
|
-
const args = { ...this.#args, ...bodyArgs };
|
|
321
|
-
this.#rawBlock += body + closeTag;
|
|
322
|
-
this.#emitCompletedStringDeltas(bodyArgs, events);
|
|
323
|
-
events.push({ type: "toolEnd", id: this.#id, name: this.#name, arguments: args, rawBlock: this.#rawBlock });
|
|
324
|
-
this.#buffer = this.#buffer.slice(close + closeTag.length);
|
|
325
|
-
this.#reset();
|
|
326
|
-
return true;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
#emitCompletedStringDeltas(args: Record<string, unknown>, events: InbandScanEvent[]): void {
|
|
331
|
-
for (const key in args) {
|
|
332
|
-
const value = args[key];
|
|
333
|
-
if (typeof value === "string" && value.length > 0) {
|
|
334
|
-
events.push({ type: "toolArgDelta", id: this.#id, name: this.#name, key, delta: value });
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
#stripInlineLeadingDelimiter(final: boolean): void {
|
|
340
|
-
if (!this.#inlineLeading) return;
|
|
270
|
+
#stripBodyLeading(final: boolean): void {
|
|
271
|
+
if (!this.#bodyLeading) return;
|
|
341
272
|
if (this.#buffer.length === 0) return;
|
|
342
273
|
if (this.#buffer[0] === "\r") {
|
|
343
274
|
if (this.#buffer.length === 1 && !final) return;
|
|
@@ -345,24 +276,55 @@ export class PiNativeInbandScanner implements InbandScanner {
|
|
|
345
276
|
this.#rawBlock += this.#buffer.slice(0, 2);
|
|
346
277
|
this.#buffer = this.#buffer.slice(2);
|
|
347
278
|
}
|
|
348
|
-
this.#
|
|
279
|
+
this.#bodyLeading = false;
|
|
349
280
|
return;
|
|
350
281
|
}
|
|
351
282
|
if (this.#buffer[0] === "\n") {
|
|
352
283
|
this.#rawBlock += this.#buffer[0];
|
|
353
284
|
this.#buffer = this.#buffer.slice(1);
|
|
354
285
|
}
|
|
355
|
-
this.#
|
|
286
|
+
this.#bodyLeading = false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
#emitBodyDelta(delta: string, events: InbandScanEvent[]): void {
|
|
290
|
+
if (delta.length === 0) return;
|
|
291
|
+
this.#bodyValue += delta;
|
|
292
|
+
events.push({ type: "toolArgDelta", id: this.#id, name: this.#name, key: this.#bodyKey, delta });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#consumeThinking(events: InbandScanEvent[], final: boolean): boolean {
|
|
296
|
+
const close = this.#buffer.indexOf(THINK_SIGIL);
|
|
297
|
+
if (close === -1) {
|
|
298
|
+
const hold = final ? 0 : partialSuffixOverlapAny(this.#buffer, [THINK_SIGIL]);
|
|
299
|
+
this.#emitThinking(this.#buffer.slice(0, this.#buffer.length - hold), events);
|
|
300
|
+
this.#buffer = this.#buffer.slice(this.#buffer.length - hold);
|
|
301
|
+
if (final) {
|
|
302
|
+
this.#endThinking(events);
|
|
303
|
+
this.#state = "outside";
|
|
304
|
+
}
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
this.#emitThinking(this.#buffer.slice(0, close), events);
|
|
308
|
+
this.#buffer = this.#buffer.slice(close + THINK_SIGIL.length);
|
|
309
|
+
this.#endThinking(events);
|
|
310
|
+
this.#state = "outside";
|
|
311
|
+
return true;
|
|
356
312
|
}
|
|
357
313
|
|
|
358
|
-
#
|
|
314
|
+
#emitThinking(delta: string, events: InbandScanEvent[]): void {
|
|
359
315
|
if (delta.length === 0) return;
|
|
360
|
-
this.#
|
|
361
|
-
events.push({ type: "
|
|
316
|
+
this.#thinking += delta;
|
|
317
|
+
events.push({ type: "thinkingDelta", delta });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
#endThinking(events: InbandScanEvent[]): void {
|
|
321
|
+
events.push({ type: "thinkingEnd", thinking: this.#thinking });
|
|
322
|
+
this.#thinking = "";
|
|
323
|
+
this.#state = "outside";
|
|
362
324
|
}
|
|
363
325
|
|
|
364
326
|
#inlineTargetKey(): string | undefined {
|
|
365
|
-
const shape = this.#
|
|
327
|
+
const shape = this.#argShapes.get(this.#name);
|
|
366
328
|
if (shape) {
|
|
367
329
|
for (const key of shape.parameterOrder) {
|
|
368
330
|
if (Object.hasOwn(this.#args, key)) continue;
|
|
@@ -370,258 +332,149 @@ export class PiNativeInbandScanner implements InbandScanner {
|
|
|
370
332
|
}
|
|
371
333
|
return undefined;
|
|
372
334
|
}
|
|
373
|
-
|
|
374
335
|
for (const key of this.#stringArgs(this.#name)) {
|
|
375
336
|
if (!Object.hasOwn(this.#args, key)) return key;
|
|
376
337
|
}
|
|
377
338
|
return "input";
|
|
378
339
|
}
|
|
379
340
|
|
|
380
|
-
#shape(): ToolArgShape | undefined {
|
|
381
|
-
return this.#argShapes.get(this.#name);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
#closeTag(): string {
|
|
385
|
-
return `</call:${this.#name}>`;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
341
|
#reset(): void {
|
|
389
342
|
this.#state = "outside";
|
|
390
|
-
this.#bodyMode = "undecided";
|
|
391
343
|
this.#id = "";
|
|
392
344
|
this.#name = "";
|
|
393
345
|
this.#args = {};
|
|
394
|
-
this.#
|
|
395
|
-
this.#
|
|
396
|
-
this.#
|
|
346
|
+
this.#bodyKey = "";
|
|
347
|
+
this.#bodyValue = "";
|
|
348
|
+
this.#bodyLeading = false;
|
|
349
|
+
this.#closeMarker = "";
|
|
397
350
|
this.#rawBlock = "";
|
|
398
351
|
}
|
|
399
352
|
}
|
|
400
353
|
|
|
401
|
-
function
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
endTag: string | undefined,
|
|
405
|
-
properties: Record<string, unknown>,
|
|
406
|
-
): MembersResult {
|
|
407
|
-
const value: Record<string, unknown> = {};
|
|
408
|
-
let index = position;
|
|
354
|
+
function parseHeaderArgs(text: string, properties: Record<string, unknown>): Record<string, unknown> {
|
|
355
|
+
const args: Record<string, unknown> = {};
|
|
356
|
+
let index = skipWhitespace(text, 0);
|
|
409
357
|
while (index < text.length) {
|
|
410
|
-
index = skipWhitespace(text, index);
|
|
411
|
-
if (endTag && text.startsWith(endTag, index)) return { ok: true, value, next: index + endTag.length };
|
|
412
|
-
if (index >= text.length) break;
|
|
413
|
-
if (text[index] !== "<" || text.startsWith("</", index) || text.startsWith(CALL_PREFIX, index)) {
|
|
414
|
-
return { ok: false, value, next: index };
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const tag = parseElementOpenTag(text, index);
|
|
418
|
-
if (!tag) return { ok: false, value, next: index };
|
|
419
|
-
const propertySchema = properties[tag.name];
|
|
420
|
-
const schemaArray = isArraySchema(propertySchema);
|
|
421
|
-
const itemSchema = schemaArray ? getArrayItemSchema(propertySchema) : propertySchema;
|
|
422
|
-
const parsed = parseElementValue(text, tag, itemSchema);
|
|
423
|
-
if (!parsed.ok) return { ok: false, value, next: index };
|
|
424
|
-
addMember(value, tag.name, parsed.value, schemaArray);
|
|
425
|
-
index = parsed.next;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
return endTag ? { ok: false, value, next: index } : { ok: true, value, next: index };
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function parseElementValue(text: string, tag: OpenTag, schema: unknown): ValueResult {
|
|
432
|
-
const attrProperties = getObjectProperties(schema);
|
|
433
|
-
const attrs = coerceAttributes(tag.rawAttrs, attrProperties);
|
|
434
|
-
if (tag.selfClosing) {
|
|
435
|
-
if (isObjectSchema(schema) || tag.rawAttrs.length > 0) return { ok: true, value: attrs, next: tag.end };
|
|
436
|
-
return { ok: true, value: coerceValue("", schema), next: tag.end };
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
const bodyStart = tag.end;
|
|
440
|
-
const closeTag = `</${tag.name}>`;
|
|
441
|
-
if (shouldParseObjectBody(text, bodyStart, closeTag, schema, tag.rawAttrs.length > 0)) {
|
|
442
|
-
const parsed = parseMembers(text, bodyStart, closeTag, attrProperties);
|
|
443
|
-
if (!parsed.ok) return { ok: false, value: undefined, next: bodyStart };
|
|
444
|
-
return { ok: true, value: { ...attrs, ...parsed.value }, next: parsed.next };
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const close = text.indexOf(closeTag, bodyStart);
|
|
448
|
-
if (close === -1) return { ok: false, value: undefined, next: bodyStart };
|
|
449
|
-
const raw = stripBlockDelimiters(text.slice(bodyStart, close));
|
|
450
|
-
return { ok: true, value: coerceValue(raw, schema), next: close + closeTag.length };
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function shouldParseObjectBody(
|
|
454
|
-
text: string,
|
|
455
|
-
bodyStart: number,
|
|
456
|
-
closeTag: string,
|
|
457
|
-
schema: unknown,
|
|
458
|
-
hasAttrs: boolean,
|
|
459
|
-
): boolean {
|
|
460
|
-
if (isObjectSchema(schema)) return true;
|
|
461
|
-
if (isTypedScalarSchema(schema)) return false;
|
|
462
|
-
if (hasAttrs) return true;
|
|
463
|
-
const first = skipWhitespace(text, bodyStart);
|
|
464
|
-
if (text.startsWith(closeTag, first)) return false;
|
|
465
|
-
return text[first] === "<" && !text.startsWith("</", first) && !text.startsWith(CALL_PREFIX, first);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function isTypedScalarSchema(schema: unknown): boolean {
|
|
469
|
-
const types = collectSchemaTypes(schema);
|
|
470
|
-
if (types.size === 0) return false;
|
|
471
|
-
return !types.has("object") && !types.has("array");
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function addMember(target: Record<string, unknown>, key: string, value: unknown, schemaArray: boolean): void {
|
|
475
|
-
if (schemaArray) {
|
|
476
|
-
const existing = target[key];
|
|
477
|
-
if (Array.isArray(existing)) existing.push(value);
|
|
478
|
-
else target[key] = [value];
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (!Object.hasOwn(target, key)) {
|
|
483
|
-
target[key] = value;
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
const existing = target[key];
|
|
488
|
-
if (Array.isArray(existing)) existing.push(value);
|
|
489
|
-
else target[key] = [existing, value];
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function coerceAttributes(
|
|
493
|
-
rawAttrs: readonly RawAttribute[],
|
|
494
|
-
properties: Record<string, unknown>,
|
|
495
|
-
): Record<string, unknown> {
|
|
496
|
-
const attrs: Record<string, unknown> = {};
|
|
497
|
-
for (const attr of rawAttrs) {
|
|
498
|
-
attrs[attr.name] = attr.value === true ? true : coerceValue(attr.value, properties[attr.name]);
|
|
499
|
-
}
|
|
500
|
-
return attrs;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
function parseCallOpenTag(text: string): OpenTag | undefined {
|
|
504
|
-
if (!text.startsWith(CALL_PREFIX)) return undefined;
|
|
505
|
-
const tagEnd = findTagEnd(text, 0);
|
|
506
|
-
if (tagEnd === -1) return undefined;
|
|
507
|
-
return parseOpenTagContent(text, CALL_PREFIX.length, tagEnd);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function parseElementOpenTag(text: string, start: number): OpenTag | undefined {
|
|
511
|
-
if (text[start] !== "<" || text.startsWith("</", start) || text.startsWith(CALL_PREFIX, start)) return undefined;
|
|
512
|
-
const tagEnd = findTagEnd(text, start);
|
|
513
|
-
if (tagEnd === -1) return undefined;
|
|
514
|
-
return parseOpenTagContent(text, start + 1, tagEnd);
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function parseOpenTagContent(text: string, contentStart: number, tagEnd: number): OpenTag | undefined {
|
|
518
|
-
let contentEnd = tagEnd;
|
|
519
|
-
let cursor = skipWhitespace(text, contentStart);
|
|
520
|
-
const nameStart = cursor;
|
|
521
|
-
if (!isNameStart(text[cursor])) return undefined;
|
|
522
|
-
cursor++;
|
|
523
|
-
while (cursor < contentEnd && isNameChar(text[cursor])) cursor++;
|
|
524
|
-
const name = text.slice(nameStart, cursor);
|
|
525
|
-
|
|
526
|
-
let selfClosing = false;
|
|
527
|
-
let last = contentEnd - 1;
|
|
528
|
-
while (last >= cursor && isWhitespace(text[last])) last--;
|
|
529
|
-
if (text[last] === "/") {
|
|
530
|
-
selfClosing = true;
|
|
531
|
-
contentEnd = last;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
return {
|
|
535
|
-
name,
|
|
536
|
-
rawAttrs: parseRawAttributes(text.slice(cursor, contentEnd)),
|
|
537
|
-
selfClosing,
|
|
538
|
-
end: tagEnd + 1,
|
|
539
|
-
};
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function parseRawAttributes(text: string): RawAttribute[] {
|
|
543
|
-
const attrs: RawAttribute[] = [];
|
|
544
|
-
let index = 0;
|
|
545
|
-
while (index < text.length) {
|
|
546
|
-
index = skipWhitespace(text, index);
|
|
547
|
-
if (index >= text.length) break;
|
|
548
358
|
if (!isNameStart(text[index])) {
|
|
549
359
|
index++;
|
|
550
360
|
continue;
|
|
551
361
|
}
|
|
552
|
-
|
|
553
362
|
const nameStart = index;
|
|
554
363
|
index++;
|
|
555
364
|
while (index < text.length && isNameChar(text[index])) index++;
|
|
556
|
-
const
|
|
365
|
+
const key = text.slice(nameStart, index);
|
|
557
366
|
index = skipWhitespace(text, index);
|
|
558
367
|
if (text[index] !== "=") {
|
|
559
|
-
|
|
368
|
+
args[key] = true;
|
|
560
369
|
continue;
|
|
561
370
|
}
|
|
371
|
+
index = skipWhitespace(text, index + 1);
|
|
372
|
+
const parsed = readInlineValue(text, index, properties[key]);
|
|
373
|
+
args[key] = parsed.value;
|
|
374
|
+
index = skipWhitespace(text, parsed.next);
|
|
375
|
+
}
|
|
376
|
+
return args;
|
|
377
|
+
}
|
|
562
378
|
|
|
563
|
-
|
|
564
|
-
index = skipWhitespace(text, index);
|
|
565
|
-
if (index >= text.length) {
|
|
566
|
-
attrs.push({ name, value: "" });
|
|
567
|
-
break;
|
|
568
|
-
}
|
|
379
|
+
type InlineValue = { value: unknown; next: number };
|
|
569
380
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
381
|
+
function readInlineValue(text: string, start: number, schema: unknown): InlineValue {
|
|
382
|
+
const ch = text[start];
|
|
383
|
+
if (ch === '"') {
|
|
384
|
+
let index = start + 1;
|
|
385
|
+
while (index < text.length) {
|
|
386
|
+
const c = text[index];
|
|
387
|
+
if (c === "\\") {
|
|
388
|
+
index += 2;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (c === '"') {
|
|
392
|
+
index++;
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
index++;
|
|
396
|
+
}
|
|
397
|
+
const raw = text.slice(start, index);
|
|
398
|
+
try {
|
|
399
|
+
return { value: JSON.parse(raw) as unknown, next: index };
|
|
400
|
+
} catch {
|
|
401
|
+
return { value: raw.slice(1, raw.endsWith('"') ? -1 : undefined), next: index };
|
|
577
402
|
}
|
|
578
|
-
|
|
579
|
-
const valueStart = index;
|
|
580
|
-
while (index < text.length && !isWhitespace(text[index])) index++;
|
|
581
|
-
attrs.push({ name, value: text.slice(valueStart, index) });
|
|
582
403
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
return isWhitespace(next) || next === "/" || next === ">" ? text.slice(start, index) : "";
|
|
404
|
+
if (ch === "[" || ch === "{") {
|
|
405
|
+
const end = matchBracket(text, start);
|
|
406
|
+
const raw = text.slice(start, end);
|
|
407
|
+
try {
|
|
408
|
+
return { value: JSON.parse(raw) as unknown, next: end };
|
|
409
|
+
} catch {
|
|
410
|
+
return { value: raw, next: end };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
let index = start;
|
|
414
|
+
while (index < text.length && !isWhitespace(text[index])) index++;
|
|
415
|
+
return { value: coerceValue(text.slice(start, index), schema), next: index };
|
|
596
416
|
}
|
|
597
417
|
|
|
598
|
-
function
|
|
599
|
-
let
|
|
418
|
+
function matchBracket(text: string, start: number): number {
|
|
419
|
+
let depth = 0;
|
|
420
|
+
let inString = false;
|
|
600
421
|
for (let index = start; index < text.length; index++) {
|
|
601
422
|
const ch = text[index];
|
|
602
|
-
if (
|
|
603
|
-
if (ch ===
|
|
423
|
+
if (inString) {
|
|
424
|
+
if (ch === "\\") {
|
|
425
|
+
index++;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (ch === '"') inString = false;
|
|
604
429
|
continue;
|
|
605
430
|
}
|
|
606
|
-
if (ch === '"'
|
|
607
|
-
|
|
431
|
+
if (ch === '"') {
|
|
432
|
+
inString = true;
|
|
608
433
|
continue;
|
|
609
434
|
}
|
|
610
|
-
if (ch === "
|
|
435
|
+
if (ch === "[" || ch === "{") depth++;
|
|
436
|
+
else if (ch === "]" || ch === "}") {
|
|
437
|
+
depth--;
|
|
438
|
+
if (depth === 0) return index + 1;
|
|
439
|
+
}
|
|
611
440
|
}
|
|
612
|
-
return
|
|
441
|
+
return text.length;
|
|
613
442
|
}
|
|
614
443
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
444
|
+
// Locate where a call header ends: the first top-level body fence, the first
|
|
445
|
+
// literal newline (scalar-only call), end-of-input, or "incomplete" when a
|
|
446
|
+
// quoted/bracketed value is still mid-stream.
|
|
447
|
+
function findHeaderEnd(text: string, start: number): HeaderEnd {
|
|
448
|
+
let inString = false;
|
|
449
|
+
let depth = 0;
|
|
450
|
+
for (let index = start; index < text.length; index++) {
|
|
451
|
+
const ch = text[index];
|
|
452
|
+
if (inString) {
|
|
453
|
+
if (ch === "\\") {
|
|
454
|
+
index++;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (ch === '"') inString = false;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (ch === '"') {
|
|
461
|
+
inString = true;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (ch === "[" || ch === "{") {
|
|
465
|
+
depth++;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (ch === "]" || ch === "}") {
|
|
469
|
+
if (depth > 0) depth--;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (depth > 0) continue;
|
|
473
|
+
if (ch === FENCE_OPEN) return { kind: "fence", index };
|
|
474
|
+
if (ch === "\n" || ch === "\r") return { kind: "newline", index };
|
|
623
475
|
}
|
|
624
|
-
|
|
476
|
+
if (inString || depth > 0) return { kind: "incomplete" };
|
|
477
|
+
return { kind: "eof", index: text.length };
|
|
625
478
|
}
|
|
626
479
|
|
|
627
480
|
function skipWhitespace(text: string, index: number): number {
|
|
@@ -645,26 +498,83 @@ function renderToolCall(call: ToolCall, options: DialectRenderOptions = {}): str
|
|
|
645
498
|
return renderInvocation(call, buildArgShapes(options.tools).get(call.name));
|
|
646
499
|
}
|
|
647
500
|
|
|
501
|
+
function renderAssistantToolCalls(calls: readonly ToolCall[], options: DialectRenderOptions = {}): string {
|
|
502
|
+
const shapes = buildArgShapes(options.tools);
|
|
503
|
+
return calls.map(call => renderInvocation(call, shapes.get(call.name))).join("\n");
|
|
504
|
+
}
|
|
505
|
+
|
|
648
506
|
function renderInvocation(call: ToolCall, shape: ToolArgShape | undefined): string {
|
|
649
|
-
|
|
507
|
+
const properties = shape?.properties ?? {};
|
|
508
|
+
const bodyKey = selectBodyKey(call.arguments, shape);
|
|
509
|
+
let header = `${CALL_SIGIL}${call.name}`;
|
|
650
510
|
for (const key in call.arguments) {
|
|
651
|
-
|
|
511
|
+
if (key === bodyKey) continue;
|
|
512
|
+
header += ` ${key}=${renderInlineValue(call.arguments[key], properties[key])}`;
|
|
652
513
|
}
|
|
653
|
-
return
|
|
514
|
+
if (bodyKey === undefined) return header;
|
|
515
|
+
const body = String(call.arguments[bodyKey]);
|
|
516
|
+
const fence = 1 + maxRun(body, FENCE_CLOSE);
|
|
517
|
+
return `${header}${FENCE_OPEN.repeat(fence)}\n${body}\n${FENCE_CLOSE.repeat(fence)}`;
|
|
654
518
|
}
|
|
655
519
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
520
|
+
// The body holds a single dominant string argument: the first string-only
|
|
521
|
+
// parameter whose value contains a newline. Single-line strings stay inline
|
|
522
|
+
// (quoted when needed) so the verbatim fence is reserved for genuine blocks.
|
|
523
|
+
function selectBodyKey(args: Record<string, unknown>, shape: ToolArgShape | undefined): string | undefined {
|
|
524
|
+
// Round-trip requires renderer and scanner to agree on the omitted body key.
|
|
525
|
+
// The scanner assigns the body to the first string-only parameter missing from
|
|
526
|
+
// the header, so a body is only safe when no earlier parameter is also absent
|
|
527
|
+
// (no schema → keep everything inline).
|
|
528
|
+
if (!shape) return undefined;
|
|
529
|
+
for (const key of shape.parameterOrder) {
|
|
530
|
+
if (!Object.hasOwn(args, key)) return undefined;
|
|
531
|
+
const value = args[key];
|
|
532
|
+
if (typeof value === "string" && value.includes("\n") && isStringOnlySchema(shape.properties[key])) return key;
|
|
533
|
+
}
|
|
534
|
+
return undefined;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function renderInlineValue(value: unknown, schema: unknown): string {
|
|
538
|
+
if (typeof value === "string") {
|
|
539
|
+
return needsQuote(value) ? JSON.stringify(value) : value;
|
|
540
|
+
}
|
|
541
|
+
if (isStringOnlySchema(schema) && value === null) return '""';
|
|
542
|
+
return stringifyJson(value);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function needsQuote(value: string): boolean {
|
|
546
|
+
if (value.length === 0) return true;
|
|
547
|
+
const first = value[0];
|
|
548
|
+
if (first === '"' || first === "[" || first === "{") return true;
|
|
549
|
+
return /[\s«»]/.test(value);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function maxRun(text: string, ch: string): number {
|
|
553
|
+
let best = 0;
|
|
554
|
+
let run = 0;
|
|
555
|
+
for (let index = 0; index < text.length; index++) {
|
|
556
|
+
if (text[index] === ch) {
|
|
557
|
+
run++;
|
|
558
|
+
if (run > best) best = run;
|
|
559
|
+
} else {
|
|
560
|
+
run = 0;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return best;
|
|
659
564
|
}
|
|
660
565
|
|
|
661
566
|
function renderToolResults(results: readonly DialectToolResult[], _options?: DialectRenderOptions): string {
|
|
662
|
-
return
|
|
567
|
+
return results
|
|
568
|
+
.map(result => {
|
|
569
|
+
const fence = RESULT_FENCE.repeat(Math.max(2, 1 + maxRun(result.text, RESULT_FENCE)));
|
|
570
|
+
return `${fence}\n${result.text}\n${fence}`;
|
|
571
|
+
})
|
|
572
|
+
.join("\n");
|
|
663
573
|
}
|
|
664
574
|
|
|
665
575
|
function renderThinking(text: string): string {
|
|
666
576
|
if (!text) return "";
|
|
667
|
-
return
|
|
577
|
+
return `${THINK_SIGIL}\n${text}\n${THINK_SIGIL}`;
|
|
668
578
|
}
|
|
669
579
|
|
|
670
580
|
function renderTranscript(messages: readonly Message[], options: DialectRenderOptions = {}): string {
|
|
@@ -676,29 +586,6 @@ function renderTranscript(messages: readonly Message[], options: DialectRenderOp
|
|
|
676
586
|
});
|
|
677
587
|
}
|
|
678
588
|
|
|
679
|
-
function renderElement(key: string, value: unknown, schema: unknown): string {
|
|
680
|
-
if (Array.isArray(value)) {
|
|
681
|
-
const itemSchema = getArrayItemSchema(schema);
|
|
682
|
-
return value.map(item => renderElement(key, item, itemSchema)).join("\n");
|
|
683
|
-
}
|
|
684
|
-
if (value && typeof value === "object") {
|
|
685
|
-
const record = value as Record<string, unknown>;
|
|
686
|
-
const properties = getObjectProperties(schema);
|
|
687
|
-
let body = `<${key}>`;
|
|
688
|
-
for (const childKey in record) {
|
|
689
|
-
body += `\n${renderElement(childKey, record[childKey], properties[childKey])}`;
|
|
690
|
-
}
|
|
691
|
-
return `${body}\n</${key}>`;
|
|
692
|
-
}
|
|
693
|
-
return `<${key}>${renderScalar(value, schema)}</${key}>`;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
function renderScalar(value: unknown, schema: unknown): string {
|
|
697
|
-
if (typeof value === "string") return value;
|
|
698
|
-
if (isStringOnlySchema(schema) && value === null) return "";
|
|
699
|
-
return stringifyJson(value);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
589
|
const definition: DialectDefinition = {
|
|
703
590
|
dialect: "pi",
|
|
704
591
|
prompt: dialectPrompt,
|
|
@@ -80,6 +80,13 @@ function resolveBearerToken(options: BedrockOptions): string | undefined {
|
|
|
80
80
|
return options.bearerToken || apiKey || $env.AWS_BEARER_TOKEN_BEDROCK;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
function inferRegionFromBedrockArn(modelId: string): string | undefined {
|
|
84
|
+
const parts = modelId.split(":", 6);
|
|
85
|
+
if (parts[0] !== "arn" || parts[2] !== "bedrock") return undefined;
|
|
86
|
+
const region = parts[3];
|
|
87
|
+
return region || undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
83
90
|
type Block = (TextContent | ThinkingContent | ToolCall) & {
|
|
84
91
|
index?: number;
|
|
85
92
|
partialJson?: string;
|
|
@@ -206,7 +213,12 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
206
213
|
|
|
207
214
|
const blocks = output.content as Block[];
|
|
208
215
|
let rawRequestDump: RawHttpRequestDump | undefined;
|
|
209
|
-
const region =
|
|
216
|
+
const region =
|
|
217
|
+
options.region ||
|
|
218
|
+
inferRegionFromBedrockArn(model.id) ||
|
|
219
|
+
$env.AWS_REGION ||
|
|
220
|
+
$env.AWS_DEFAULT_REGION ||
|
|
221
|
+
"us-east-1";
|
|
210
222
|
|
|
211
223
|
try {
|
|
212
224
|
const cacheRetention = resolveCacheRetention(options.cacheRetention);
|
|
@@ -19,6 +19,7 @@ const builtInOAuthProviders: OAuthProviderInfo[] = PROVIDER_REGISTRY.filter(
|
|
|
19
19
|
id: provider.id,
|
|
20
20
|
name: provider.name,
|
|
21
21
|
available: provider.available ?? true,
|
|
22
|
+
storeCredentialsAs: provider.storeCredentialsAs,
|
|
22
23
|
}));
|
|
23
24
|
|
|
24
25
|
const customOAuthProviders = new Map<string, OAuthProviderInterface>();
|
|
@@ -163,6 +164,7 @@ export function getOAuthProviders(): OAuthProviderInfo[] {
|
|
|
163
164
|
id: provider.id,
|
|
164
165
|
name: provider.name,
|
|
165
166
|
available: true,
|
|
167
|
+
storeCredentialsAs: provider.storeCredentialsAs,
|
|
166
168
|
}));
|
|
167
169
|
return [...builtInOAuthProviders, ...customProviders];
|
|
168
170
|
}
|
|
@@ -31,6 +31,12 @@ export interface OAuthProviderInfo {
|
|
|
31
31
|
id: OAuthProviderId;
|
|
32
32
|
name: string;
|
|
33
33
|
available: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Provider id the login stores credentials under, when it differs from `id`
|
|
36
|
+
* (e.g. `openai-codex-device` ⇒ `openai-codex`). Lets callers map a login
|
|
37
|
+
* entry back to the model provider it authenticates.
|
|
38
|
+
*/
|
|
39
|
+
storeCredentialsAs?: string;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
export interface OAuthController {
|