@oh-my-pi/pi-ai 16.1.1 → 16.1.3
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 -0
- package/dist/types/auth-broker/discover.d.ts +35 -0
- package/dist/types/auth-broker/index.d.ts +1 -0
- package/dist/types/providers/openai-reasoning-fallback.d.ts +25 -0
- package/dist/types/providers/openai-responses.d.ts +2 -1
- package/dist/types/types.d.ts +2 -2
- package/package.json +4 -4
- package/src/auth-broker/discover.ts +217 -0
- package/src/auth-broker/index.ts +1 -0
- package/src/auth-storage.ts +69 -27
- package/src/dialect/examples.ts +1 -1
- package/src/dialect/pi.md +1 -1
- package/src/providers/anthropic.ts +14 -3
- package/src/providers/azure-openai-responses.ts +50 -21
- package/src/providers/ollama.ts +17 -21
- package/src/providers/openai-completions.ts +52 -2
- package/src/providers/openai-reasoning-fallback.ts +269 -0
- package/src/providers/openai-responses.ts +81 -7
- package/src/types.ts +2 -2
- package/src/utils/thinking-loop.ts +18 -19
- package/src/utils/validation.ts +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.1.3] - 2026-06-19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added regression test pinning that `openai-completions` emits a `thinking` block for `reasoning_content` deltas even when `delta.content` is explicitly JSON `null` (the DeepSeek-format dual-key pattern used by custom GLM/Qwen reasoning providers). See [#2996](https://github.com/can1357/oh-my-pi/issues/2996).
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Improved the thinking loop guard to treat assistant text loops as retryable errors
|
|
14
|
+
- Refined text normalization logic to reduce false positives in the thinking loop detector
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Fixed Ollama chat requests sending image payloads to text-only models. Image blocks are now omitted and replaced with the standard non-vision placeholder for models without vision support, while vision-capable Ollama models continue to receive images. ([#3009](https://github.com/can1357/oh-my-pi/pull/3009) by [@serverinspector](https://github.com/serverinspector))
|
|
19
|
+
- Fixed `SqliteAuthCredentialStore.close()` leaking one-off prepared statements created by inline `this.#db.prepare()` calls in `#authCredentialsTableExists`, `#readAuthSchemaVersion`, `#inferAuthSchemaVersion`, `#migrateAuthSchemaV0ToV1`, `#backfillCredentialIdentityKeys`, and `updateAuthCredential`. Each statement is now wrapped in `try/finally` with `stmt.finalize()`, and the `close()` method finalizes `#insertUsageCostStmt` and `#listUsageCostsStmt` which were previously missed. This caused EBUSY on Windows when tests tried to delete temp dirs containing open SQLite handles.
|
|
20
|
+
|
|
21
|
+
## [16.1.2] - 2026-06-19
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Added improved JSON repair capabilities for Anthropic tool arguments
|
|
26
|
+
- Added authentication broker discovery to sync credentials between local SQLite and remote state
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Improved error feedback and transparency for malformed Anthropic tool call arguments
|
|
31
|
+
- Added automatic fallback for unsupported OpenAI reasoning effort levels
|
|
32
|
+
- Improved reliability when handling invalid reasoning parameter errors across OpenAI-compatible APIs
|
|
33
|
+
- Fixed OpenAI-compatible Chat Completions, Responses, and Azure Responses requests to retry once with the nearest provider-supported reasoning effort when an endpoint rejects `xhigh`/`minimal`-style effort values.
|
|
34
|
+
|
|
5
35
|
## [16.1.0] - 2026-06-19
|
|
6
36
|
|
|
7
37
|
### Added
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AuthStorage } from "../auth-storage";
|
|
2
|
+
export interface AuthBrokerClientConfig {
|
|
3
|
+
url: string;
|
|
4
|
+
token: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ResolveAuthBrokerConfigOptions {
|
|
7
|
+
agentDir?: string;
|
|
8
|
+
configValueResolver?: (config: string) => Promise<string | undefined>;
|
|
9
|
+
}
|
|
10
|
+
export interface DiscoverAuthStorageOptions {
|
|
11
|
+
agentDir?: string;
|
|
12
|
+
configValueResolver?: (config: string) => Promise<string | undefined>;
|
|
13
|
+
cachePath?: string;
|
|
14
|
+
sourceLabel?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Path to the local bearer token file. Created by `omp auth-broker token`. */
|
|
17
|
+
export declare function getAuthBrokerTokenFilePath(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve broker connection configuration using the same precedence as the TUI:
|
|
20
|
+
*
|
|
21
|
+
* 1. `OMP_AUTH_BROKER_URL` / `OMP_AUTH_BROKER_TOKEN` env vars.
|
|
22
|
+
* 2. `auth.broker.url` / `auth.broker.token` in `<agentDir>/config.yml`.
|
|
23
|
+
* 3. `<config-root>/auth-broker.token` file (paired with a URL from env/config).
|
|
24
|
+
*
|
|
25
|
+
* Returns `null` when no broker URL is configured — callers should fall back to
|
|
26
|
+
* the local SQLite store. Throws when a URL is configured but no token is
|
|
27
|
+
* available, matching the TUI behavior.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveAuthBrokerConfig(options?: ResolveAuthBrokerConfigOptions): Promise<AuthBrokerClientConfig | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Create an AuthStorage instance, using the broker when configured and falling
|
|
32
|
+
* back to the local SQLite store otherwise. This is the single source of truth
|
|
33
|
+
* for the TUI and the catalog generator.
|
|
34
|
+
*/
|
|
35
|
+
export declare function discoverAuthStorage(options?: DiscoverAuthStorageOptions): Promise<AuthStorage>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CapturedHttpErrorResponse } from "../utils/http-inspector";
|
|
2
|
+
/** @internal */
|
|
3
|
+
export type OpenAIReasoningEffortFallback = string | null;
|
|
4
|
+
/** @internal */
|
|
5
|
+
export interface OpenAIReasoningEffortFallbackState {
|
|
6
|
+
reasoningEffortFallbacks: Map<string, OpenAIReasoningEffortFallback>;
|
|
7
|
+
}
|
|
8
|
+
/** @internal */
|
|
9
|
+
export declare function createOpenAIReasoningEffortFallbackState(): OpenAIReasoningEffortFallbackState;
|
|
10
|
+
/** @internal */
|
|
11
|
+
export declare function clearOpenAIReasoningEffortFallbackState(state: OpenAIReasoningEffortFallbackState): void;
|
|
12
|
+
/** @internal */
|
|
13
|
+
export declare function getOpenAIReasoningEffortFallback(state: OpenAIReasoningEffortFallbackState | undefined, key: string): OpenAIReasoningEffortFallback | undefined;
|
|
14
|
+
/** @internal */
|
|
15
|
+
export declare function rememberOpenAIReasoningEffortFallback(state: OpenAIReasoningEffortFallbackState | undefined, key: string, fallback: OpenAIReasoningEffortFallback): void;
|
|
16
|
+
/** @internal */
|
|
17
|
+
export declare function createOpenAIReasoningEffortFallbackKey(endpoint: "chat-completions" | "responses" | "azure-responses", baseUrl: string | undefined, wireModelId: string | undefined): string;
|
|
18
|
+
/** @internal */
|
|
19
|
+
export declare function readOpenAIReasoningEffort(params: unknown): string | undefined;
|
|
20
|
+
/** @internal */
|
|
21
|
+
export declare function applyOpenAIReasoningEffortFallback(params: unknown, fallback: OpenAIReasoningEffortFallback): boolean;
|
|
22
|
+
/** @internal */
|
|
23
|
+
export declare function resolveOpenAIReasoningEffortFallback(error: unknown, captured: CapturedHttpErrorResponse | undefined, params: unknown, options?: {
|
|
24
|
+
explicitDisable?: boolean;
|
|
25
|
+
}): OpenAIReasoningEffortFallback | undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Context, Model, OpenAICompat, ProviderSessionState, ServiceTier, StreamFunction, StreamOptions, Tool, ToolChoice } from "../types";
|
|
2
2
|
import { type OpenAIResponsesToolChoice } from "../utils/tool-choice";
|
|
3
|
+
import { type OpenAIReasoningEffortFallbackState } from "./openai-reasoning-fallback";
|
|
3
4
|
import type { Tool as OpenAITool, ResponseCreateParamsStreaming, ResponseInput } from "./openai-responses-wire";
|
|
4
5
|
import { type OpenAIStrictToolsScope, type OpenAIStrictToolsState } from "./openai-shared";
|
|
5
6
|
export interface OpenAIResponsesOptions extends StreamOptions {
|
|
@@ -55,7 +56,7 @@ export interface OpenAIResponsesOptions extends StreamOptions {
|
|
|
55
56
|
*/
|
|
56
57
|
extraBody?: Record<string, unknown>;
|
|
57
58
|
}
|
|
58
|
-
interface OpenAIResponsesProviderSessionState extends ProviderSessionState, OpenAIStrictToolsState {
|
|
59
|
+
interface OpenAIResponsesProviderSessionState extends ProviderSessionState, OpenAIStrictToolsState, OpenAIReasoningEffortFallbackState {
|
|
59
60
|
nativeHistoryReplayWarmed: boolean;
|
|
60
61
|
/** Stateful `previous_response_id` chain baselines, keyed by baseUrl/model/session. */
|
|
61
62
|
chains: Map<string, OpenAIResponsesChainState>;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -549,8 +549,8 @@ export interface Tool<TParameters extends TSchema = TSchema> {
|
|
|
549
549
|
* Illustrative calls/notes; the AI layer renders them into an `<examples>`
|
|
550
550
|
* block in the model's native tool-call syntax and appends to the wire
|
|
551
551
|
* description. Author `call`/`bad`/`good` as plain argument objects WITHOUT
|
|
552
|
-
* `
|
|
553
|
-
* a placeholder `
|
|
552
|
+
* `i` — when intent tracing injects `i` into the schema, the renderer adds
|
|
553
|
+
* a placeholder `i` automatically. Type each tool's `examples` against its
|
|
554
554
|
* own schema (e.g. `readonly ToolExample<typeof schema["type"]>[]`).
|
|
555
555
|
*/
|
|
556
556
|
examples?: readonly ToolExample[];
|
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.1.
|
|
4
|
+
"version": "16.1.3",
|
|
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,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.0",
|
|
41
|
-
"@oh-my-pi/pi-catalog": "16.1.
|
|
42
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
43
|
-
"@oh-my-pi/pi-wire": "16.1.
|
|
41
|
+
"@oh-my-pi/pi-catalog": "16.1.3",
|
|
42
|
+
"@oh-my-pi/pi-utils": "16.1.3",
|
|
43
|
+
"@oh-my-pi/pi-wire": "16.1.3",
|
|
44
44
|
"arktype": "^2.2.0",
|
|
45
45
|
"partial-json": "^0.1.7",
|
|
46
46
|
"zod": "^4"
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broker-aware auth-storage discovery used by both the coding-agent runtime and
|
|
3
|
+
* the catalog model generator. Keeps the precedence logic (env → config.yml →
|
|
4
|
+
* token file → local SQLite) in one place so build-time tooling sees the same
|
|
5
|
+
* credentials as the TUI.
|
|
6
|
+
*/
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
getAgentDbPath,
|
|
10
|
+
getAgentDir,
|
|
11
|
+
getAuthBrokerSnapshotCachePath,
|
|
12
|
+
getConfigRootDir,
|
|
13
|
+
isEnoent,
|
|
14
|
+
logger,
|
|
15
|
+
} from "@oh-my-pi/pi-utils";
|
|
16
|
+
import { YAML } from "bun";
|
|
17
|
+
import { AuthStorage } from "../auth-storage";
|
|
18
|
+
import { AuthBrokerClient } from "./client";
|
|
19
|
+
import { RemoteAuthCredentialStore } from "./remote-store";
|
|
20
|
+
import { readAuthBrokerSnapshotCache, writeAuthBrokerSnapshotCache } from "./snapshot-cache";
|
|
21
|
+
import { DEFAULT_SNAPSHOT_CACHE_TTL_MS, type SnapshotResponse } from "./types";
|
|
22
|
+
|
|
23
|
+
export interface AuthBrokerClientConfig {
|
|
24
|
+
url: string;
|
|
25
|
+
token: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ResolveAuthBrokerConfigOptions {
|
|
29
|
+
agentDir?: string;
|
|
30
|
+
configValueResolver?: (config: string) => Promise<string | undefined>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DiscoverAuthStorageOptions {
|
|
34
|
+
agentDir?: string;
|
|
35
|
+
configValueResolver?: (config: string) => Promise<string | undefined>;
|
|
36
|
+
cachePath?: string;
|
|
37
|
+
sourceLabel?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Path to the local bearer token file. Created by `omp auth-broker token`. */
|
|
41
|
+
export function getAuthBrokerTokenFilePath(): string {
|
|
42
|
+
return path.join(getConfigRootDir(), "auth-broker.token");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Default resolver for config values: checks `process.env` first, then treats
|
|
47
|
+
* the value as a literal. Does NOT execute `!command` syntax; such values are
|
|
48
|
+
* left unresolved so the caller can fall back to the token file.
|
|
49
|
+
*/
|
|
50
|
+
async function defaultResolveConfigValue(config: string): Promise<string | undefined> {
|
|
51
|
+
if (config.startsWith("!")) return undefined;
|
|
52
|
+
const envValue = process.env[config];
|
|
53
|
+
return envValue || config;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function readTokenFile(): Promise<string | null> {
|
|
57
|
+
try {
|
|
58
|
+
const raw = await Bun.file(getAuthBrokerTokenFilePath()).text();
|
|
59
|
+
const trimmed = raw.trim();
|
|
60
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (isEnoent(err)) return null;
|
|
63
|
+
logger.warn("auth-broker token file unreadable", { error: String(err) });
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface ConfigSnapshot {
|
|
69
|
+
url?: string;
|
|
70
|
+
token?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function readConfigYaml(agentDir: string): Promise<ConfigSnapshot> {
|
|
74
|
+
const configPath = path.join(agentDir, "config.yml");
|
|
75
|
+
try {
|
|
76
|
+
const raw = await Bun.file(configPath).text();
|
|
77
|
+
const parsed = YAML.parse(raw);
|
|
78
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
79
|
+
const record = parsed as Record<string, unknown>;
|
|
80
|
+
const url = typeof record["auth.broker.url"] === "string" ? (record["auth.broker.url"] as string) : undefined;
|
|
81
|
+
const token =
|
|
82
|
+
typeof record["auth.broker.token"] === "string" ? (record["auth.broker.token"] as string) : undefined;
|
|
83
|
+
return { url, token };
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (isEnoent(err)) return {};
|
|
86
|
+
logger.warn("auth-broker config.yml unreadable", { error: String(err) });
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolveSnapshotTtlMs(): number {
|
|
92
|
+
const raw = process.env.OMP_AUTH_BROKER_SNAPSHOT_TTL_MS;
|
|
93
|
+
if (raw === undefined) return DEFAULT_SNAPSHOT_CACHE_TTL_MS;
|
|
94
|
+
const value = raw.trim();
|
|
95
|
+
if (value === "") return DEFAULT_SNAPSHOT_CACHE_TTL_MS;
|
|
96
|
+
const ttlMs = Number(value);
|
|
97
|
+
if (Number.isFinite(ttlMs) && ttlMs >= 0) return ttlMs;
|
|
98
|
+
logger.warn("Invalid OMP_AUTH_BROKER_SNAPSHOT_TTL_MS; using default", { value: raw });
|
|
99
|
+
return DEFAULT_SNAPSHOT_CACHE_TTL_MS;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve broker connection configuration using the same precedence as the TUI:
|
|
104
|
+
*
|
|
105
|
+
* 1. `OMP_AUTH_BROKER_URL` / `OMP_AUTH_BROKER_TOKEN` env vars.
|
|
106
|
+
* 2. `auth.broker.url` / `auth.broker.token` in `<agentDir>/config.yml`.
|
|
107
|
+
* 3. `<config-root>/auth-broker.token` file (paired with a URL from env/config).
|
|
108
|
+
*
|
|
109
|
+
* Returns `null` when no broker URL is configured — callers should fall back to
|
|
110
|
+
* the local SQLite store. Throws when a URL is configured but no token is
|
|
111
|
+
* available, matching the TUI behavior.
|
|
112
|
+
*/
|
|
113
|
+
export async function resolveAuthBrokerConfig(
|
|
114
|
+
options: ResolveAuthBrokerConfigOptions = {},
|
|
115
|
+
): Promise<AuthBrokerClientConfig | null> {
|
|
116
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
117
|
+
const resolveConfig = options.configValueResolver ?? defaultResolveConfigValue;
|
|
118
|
+
|
|
119
|
+
const envUrl = process.env.OMP_AUTH_BROKER_URL;
|
|
120
|
+
const envToken = process.env.OMP_AUTH_BROKER_TOKEN;
|
|
121
|
+
|
|
122
|
+
let url = envUrl && envUrl.length > 0 ? envUrl : undefined;
|
|
123
|
+
let configToken: string | undefined;
|
|
124
|
+
if (!url || !envToken) {
|
|
125
|
+
const fromConfig = await readConfigYaml(agentDir);
|
|
126
|
+
if (!url && fromConfig.url) {
|
|
127
|
+
const resolved = await resolveConfig(fromConfig.url);
|
|
128
|
+
if (resolved && resolved.length > 0) url = resolved;
|
|
129
|
+
}
|
|
130
|
+
if (fromConfig.token) {
|
|
131
|
+
const resolved = await resolveConfig(fromConfig.token);
|
|
132
|
+
if (resolved && resolved.length > 0) configToken = resolved;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!url) return null;
|
|
136
|
+
|
|
137
|
+
const token =
|
|
138
|
+
(envToken && envToken.length > 0 ? envToken : undefined) ?? configToken ?? (await readTokenFile()) ?? undefined;
|
|
139
|
+
if (!token) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`OMP_AUTH_BROKER_URL is set (${url}) but no bearer token is available. ` +
|
|
142
|
+
`Set OMP_AUTH_BROKER_TOKEN, the \`auth.broker.token\` config entry, or place one at ${getAuthBrokerTokenFilePath()}.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return { url, token };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Create an AuthStorage instance, using the broker when configured and falling
|
|
150
|
+
* back to the local SQLite store otherwise. This is the single source of truth
|
|
151
|
+
* for the TUI and the catalog generator.
|
|
152
|
+
*/
|
|
153
|
+
export async function discoverAuthStorage(options: DiscoverAuthStorageOptions = {}): Promise<AuthStorage> {
|
|
154
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
155
|
+
const brokerConfig = await resolveAuthBrokerConfig({
|
|
156
|
+
agentDir,
|
|
157
|
+
configValueResolver: options.configValueResolver,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (brokerConfig) {
|
|
161
|
+
const client = new AuthBrokerClient({ url: brokerConfig.url, token: brokerConfig.token });
|
|
162
|
+
const cachePath = options.cachePath ?? getAuthBrokerSnapshotCachePath();
|
|
163
|
+
const ttlMs = resolveSnapshotTtlMs();
|
|
164
|
+
const persist =
|
|
165
|
+
ttlMs > 0
|
|
166
|
+
? (snapshot: SnapshotResponse): void => {
|
|
167
|
+
void writeAuthBrokerSnapshotCache({
|
|
168
|
+
path: cachePath,
|
|
169
|
+
token: brokerConfig.token,
|
|
170
|
+
url: brokerConfig.url,
|
|
171
|
+
snapshot,
|
|
172
|
+
}).catch(error => {
|
|
173
|
+
logger.debug("auth-broker snapshot cache write failed", { error: String(error) });
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
: undefined;
|
|
177
|
+
|
|
178
|
+
let initialSnapshot: SnapshotResponse | undefined;
|
|
179
|
+
if (ttlMs > 0) {
|
|
180
|
+
initialSnapshot =
|
|
181
|
+
(await readAuthBrokerSnapshotCache({
|
|
182
|
+
path: cachePath,
|
|
183
|
+
token: brokerConfig.token,
|
|
184
|
+
url: brokerConfig.url,
|
|
185
|
+
ttlMs,
|
|
186
|
+
}).catch(error => {
|
|
187
|
+
logger.debug("auth-broker snapshot cache read failed", { error: String(error) });
|
|
188
|
+
return null;
|
|
189
|
+
})) ?? undefined;
|
|
190
|
+
}
|
|
191
|
+
if (!initialSnapshot) {
|
|
192
|
+
const initialResult = await client.fetchSnapshot();
|
|
193
|
+
if (initialResult.status !== 200) throw new Error("Auth broker returned no initial snapshot");
|
|
194
|
+
initialSnapshot = initialResult.snapshot;
|
|
195
|
+
persist?.(initialSnapshot);
|
|
196
|
+
}
|
|
197
|
+
const store = new RemoteAuthCredentialStore({
|
|
198
|
+
client,
|
|
199
|
+
initialSnapshot,
|
|
200
|
+
onSnapshot: persist,
|
|
201
|
+
});
|
|
202
|
+
const storage = new AuthStorage(store, {
|
|
203
|
+
configValueResolver: options.configValueResolver,
|
|
204
|
+
sourceLabel: options.sourceLabel ?? `broker ${brokerConfig.url}`,
|
|
205
|
+
});
|
|
206
|
+
await storage.reload();
|
|
207
|
+
return storage;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const dbPath = getAgentDbPath(agentDir);
|
|
211
|
+
const storage = await AuthStorage.create(dbPath, {
|
|
212
|
+
configValueResolver: options.configValueResolver,
|
|
213
|
+
sourceLabel: options.sourceLabel ?? `local ${dbPath}`,
|
|
214
|
+
});
|
|
215
|
+
await storage.reload();
|
|
216
|
+
return storage;
|
|
217
|
+
}
|
package/src/auth-broker/index.ts
CHANGED
package/src/auth-storage.ts
CHANGED
|
@@ -4742,25 +4742,47 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4742
4742
|
}
|
|
4743
4743
|
|
|
4744
4744
|
#authCredentialsTableExists(): boolean {
|
|
4745
|
-
const
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4745
|
+
const stmt = this.#db.prepare(
|
|
4746
|
+
"SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = 'auth_credentials'",
|
|
4747
|
+
);
|
|
4748
|
+
try {
|
|
4749
|
+
const row = stmt.get() as { present?: number } | undefined;
|
|
4750
|
+
return row?.present === 1;
|
|
4751
|
+
} finally {
|
|
4752
|
+
stmt.finalize();
|
|
4753
|
+
}
|
|
4749
4754
|
}
|
|
4750
4755
|
|
|
4751
4756
|
#readAuthSchemaVersion(): number | null {
|
|
4752
|
-
const
|
|
4753
|
-
|
|
4754
|
-
| undefined;
|
|
4755
|
-
|
|
4757
|
+
const stmt = this.#db.prepare("SELECT version FROM auth_schema_version WHERE id = 1");
|
|
4758
|
+
try {
|
|
4759
|
+
const row = stmt.get() as { version?: number } | undefined;
|
|
4760
|
+
return typeof row?.version === "number" ? row.version : null;
|
|
4761
|
+
} finally {
|
|
4762
|
+
stmt.finalize();
|
|
4763
|
+
}
|
|
4756
4764
|
}
|
|
4757
4765
|
|
|
4758
4766
|
#writeAuthSchemaVersion(version: number): void {
|
|
4759
|
-
this.#db.prepare("INSERT OR REPLACE INTO auth_schema_version(id, version) VALUES (1, ?)")
|
|
4767
|
+
const stmt = this.#db.prepare("INSERT OR REPLACE INTO auth_schema_version(id, version) VALUES (1, ?)");
|
|
4768
|
+
try {
|
|
4769
|
+
stmt.run(version);
|
|
4770
|
+
} finally {
|
|
4771
|
+
stmt.finalize();
|
|
4772
|
+
}
|
|
4760
4773
|
}
|
|
4761
4774
|
|
|
4762
4775
|
#inferAuthSchemaVersion(): number {
|
|
4763
|
-
const
|
|
4776
|
+
const stmt = this.#db.prepare("PRAGMA table_info(auth_credentials)");
|
|
4777
|
+
try {
|
|
4778
|
+
const cols = stmt.all() as Array<{ name?: string }>;
|
|
4779
|
+
return this.#inferAuthSchemaVersionFromColumns(cols);
|
|
4780
|
+
} finally {
|
|
4781
|
+
stmt.finalize();
|
|
4782
|
+
}
|
|
4783
|
+
}
|
|
4784
|
+
|
|
4785
|
+
#inferAuthSchemaVersionFromColumns(cols: Array<{ name?: string }>): number {
|
|
4764
4786
|
const hasDisabledCause = cols.some(column => column.name === "disabled_cause");
|
|
4765
4787
|
const hasIdentityKey = cols.some(column => column.name === "identity_key");
|
|
4766
4788
|
const hasAccountId = cols.some(column => column.name === "account_id");
|
|
@@ -4808,8 +4830,14 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4808
4830
|
|
|
4809
4831
|
#migrateAuthSchemaV0ToV1(): void {
|
|
4810
4832
|
const migrate = this.#db.transaction(() => {
|
|
4811
|
-
const
|
|
4812
|
-
|
|
4833
|
+
const stmt = this.#db.prepare("PRAGMA table_info(auth_credentials)");
|
|
4834
|
+
let hasDisabled = false;
|
|
4835
|
+
try {
|
|
4836
|
+
const v0Cols = stmt.all() as Array<{ name?: string }>;
|
|
4837
|
+
hasDisabled = v0Cols.some(col => col.name === "disabled");
|
|
4838
|
+
} finally {
|
|
4839
|
+
stmt.finalize();
|
|
4840
|
+
}
|
|
4813
4841
|
|
|
4814
4842
|
this.#db.run("ALTER TABLE auth_credentials RENAME TO auth_credentials_v0");
|
|
4815
4843
|
this.#db.run(`
|
|
@@ -4885,21 +4913,29 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4885
4913
|
}
|
|
4886
4914
|
|
|
4887
4915
|
#backfillCredentialIdentityKeys(): void {
|
|
4888
|
-
const
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4916
|
+
const selectRowsStmt = this.#db.prepare(
|
|
4917
|
+
"SELECT id, provider, credential_type, data, disabled_cause, identity_key FROM auth_credentials WHERE identity_key IS NULL ORDER BY id ASC",
|
|
4918
|
+
);
|
|
4919
|
+
let rows: AuthRow[];
|
|
4920
|
+
try {
|
|
4921
|
+
rows = selectRowsStmt.all() as AuthRow[];
|
|
4922
|
+
} finally {
|
|
4923
|
+
selectRowsStmt.finalize();
|
|
4924
|
+
}
|
|
4893
4925
|
if (rows.length === 0) return;
|
|
4894
4926
|
|
|
4895
4927
|
let updateIdentity: Statement | null = null;
|
|
4896
|
-
|
|
4897
|
-
const
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4928
|
+
try {
|
|
4929
|
+
for (const row of rows) {
|
|
4930
|
+
const identityKey = resolveRowCredentialIdentityKey(row.provider, row);
|
|
4931
|
+
// Rows whose identity cannot be derived stay NULL; writing NULL over
|
|
4932
|
+
// NULL would just burn a write transaction on every boot.
|
|
4933
|
+
if (identityKey === null) continue;
|
|
4934
|
+
updateIdentity ??= this.#db.prepare("UPDATE auth_credentials SET identity_key = ? WHERE id = ?");
|
|
4935
|
+
updateIdentity.run(identityKey, row.id);
|
|
4936
|
+
}
|
|
4937
|
+
} finally {
|
|
4938
|
+
updateIdentity?.finalize();
|
|
4903
4939
|
}
|
|
4904
4940
|
}
|
|
4905
4941
|
|
|
@@ -5063,9 +5099,13 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5063
5099
|
|
|
5064
5100
|
updateAuthCredential(id: number, credential: AuthCredential): void {
|
|
5065
5101
|
try {
|
|
5066
|
-
const
|
|
5067
|
-
|
|
5068
|
-
|
|
5102
|
+
const providerStmt = this.#db.prepare("SELECT provider FROM auth_credentials WHERE id = ?");
|
|
5103
|
+
let providerRow: { provider?: string } | undefined;
|
|
5104
|
+
try {
|
|
5105
|
+
providerRow = providerStmt.get(id) as { provider?: string } | undefined;
|
|
5106
|
+
} finally {
|
|
5107
|
+
providerStmt.finalize();
|
|
5108
|
+
}
|
|
5069
5109
|
const provider = providerRow?.provider ?? "";
|
|
5070
5110
|
const serialized = serializeCredential(provider, credential);
|
|
5071
5111
|
if (!serialized) return;
|
|
@@ -5334,6 +5374,8 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
5334
5374
|
this.#lastUsageHistoryStmt.finalize();
|
|
5335
5375
|
this.#listUsageHistoryStmt.finalize();
|
|
5336
5376
|
this.#updateUsageHistoryStmt.finalize();
|
|
5377
|
+
this.#insertUsageCostStmt.finalize();
|
|
5378
|
+
this.#listUsageCostsStmt.finalize();
|
|
5337
5379
|
this.#db.close();
|
|
5338
5380
|
}
|
|
5339
5381
|
}
|
package/src/dialect/examples.ts
CHANGED
|
@@ -9,7 +9,7 @@ export function renderToolExamples(tool: InbandTool, dialect: Dialect, intentFie
|
|
|
9
9
|
if (!examples?.length) return "";
|
|
10
10
|
const definition = getDialectDefinition(dialect);
|
|
11
11
|
const renderCall = (args: Record<string, unknown>): string => {
|
|
12
|
-
// When intent tracing injects `
|
|
12
|
+
// When intent tracing injects `i` into the schema, examples must show a
|
|
13
13
|
// placeholder so the model learns to emit it. Keep it first, matching the
|
|
14
14
|
// schema injection order.
|
|
15
15
|
const finalArgs = intentField ? { [intentField]: INTENT_PLACEHOLDER, ...args } : args;
|
package/src/dialect/pi.md
CHANGED
|
@@ -23,7 +23,7 @@ Call with a verbatim body — everything between `«` and `»` is taken literall
|
|
|
23
23
|
|
|
24
24
|
Argument values:
|
|
25
25
|
|
|
26
|
-
- Strings are written bare and verbatim (`path=src/a.ts`). Quote with `"…"` only when the value contains spaces or starts with `"`, `[`, or `{` (`
|
|
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
27
|
- Numbers, booleans, and `null` are JSON literals (`offset=50`, `force=true`).
|
|
28
28
|
- Arrays and objects are inline JSON (`paths=["src","test"]`).
|
|
29
29
|
- The body fence holds the call's first long/multi-line string parameter; its key is implied, never written.
|
|
@@ -50,7 +50,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
|
50
50
|
import { isFoundryEnabled } from "../utils/foundry";
|
|
51
51
|
import { finalizeErrorMessage, type RawHttpRequestDump, rewriteCopilotError } from "../utils/http-inspector";
|
|
52
52
|
import { getStreamFirstEventTimeoutMs, getStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator";
|
|
53
|
-
import { parseStreamingJsonThrottled } from "../utils/json-parse";
|
|
53
|
+
import { parseJsonWithRepair, parseStreamingJsonThrottled } from "../utils/json-parse";
|
|
54
54
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
55
55
|
import { isCopilotTransientModelError } from "../utils/retry";
|
|
56
56
|
import { COMBINATOR_KEYS, NO_STRICT, toolWireSchema } from "../utils/schema";
|
|
@@ -1752,14 +1752,25 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1752
1752
|
const finalJson =
|
|
1753
1753
|
block.partialJson.length > 0 ? block.partialJson : JSON.stringify(block.arguments ?? {});
|
|
1754
1754
|
try {
|
|
1755
|
-
block.arguments =
|
|
1755
|
+
block.arguments = parseJsonWithRepair(finalJson) as ToolCall["arguments"];
|
|
1756
1756
|
} catch (parseError) {
|
|
1757
1757
|
// Non-fatal: keep the best-effort arguments recovered by the throttled streaming
|
|
1758
1758
|
// parser instead of failing the turn on malformed/truncated tool-argument JSON.
|
|
1759
1759
|
reportAnthropicEnvelopeAnomaly(
|
|
1760
1760
|
`tool_use ${block.id} arguments are not valid JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
|
1761
1761
|
);
|
|
1762
|
-
|
|
1762
|
+
const recoveredKeys = Object.keys(block.arguments ?? {});
|
|
1763
|
+
if (recoveredKeys.length === 0) {
|
|
1764
|
+
const maxLen = 512;
|
|
1765
|
+
const truncatedJson =
|
|
1766
|
+
finalJson.length <= maxLen
|
|
1767
|
+
? finalJson
|
|
1768
|
+
: `${finalJson.slice(0, maxLen)}… [truncated ${finalJson.length - maxLen} chars]`;
|
|
1769
|
+
block.arguments = {
|
|
1770
|
+
__parseError: parseError instanceof Error ? parseError.message : String(parseError),
|
|
1771
|
+
__rawJson: truncatedJson,
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1763
1774
|
}
|
|
1764
1775
|
delete (block as { partialJson?: string }).partialJson;
|
|
1765
1776
|
delete (block as { lastParseLen?: number }).lastParseLen;
|