@oh-my-pi/pi-ai 16.0.8 → 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 CHANGED
@@ -2,6 +2,31 @@
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
+
21
+ ## [16.0.9] - 2026-06-18
22
+
23
+ ### Fixed
24
+
25
+ - Fixed OAuth login replacing all other active accounts for the same provider, allowing multiple OAuth accounts to coexist concurrently.
26
+ - Fixed legacy `api_key` credentials not being replaced/disabled atomically upon upgrading to OAuth login.
27
+ - Fixed a logic issue where AuthStorage lost session-to-credential stickiness upon CLI restarts, causing cold-starts for server-side prompt cache (KV cache) and wasting tokens.
28
+ - Fixed GitHub Copilot Responses requests rejecting image inputs that carry the `detail: "original"` hint with an HTTP 400 by degrading the hint to `"auto"` for hosts that do not support it; other hosts still preserve native-resolution frames (snapcompact). ([#2822](https://github.com/can1357/oh-my-pi/issues/2822))
29
+
5
30
  ## [16.0.8] - 2026-06-18
6
31
 
7
32
  ### Fixed
@@ -341,11 +341,12 @@ export declare function repairOrphanResponsesToolOutputs(input: ResponseInput):
341
341
  * {@link repairOrphanResponsesToolOutputs}.
342
342
  */
343
343
  export declare function repairOrphanResponsesToolCalls(input: ResponseInput): ResponseInput;
344
- export declare function convertResponsesInputContent(content: string | Array<TextContent | ImageContent>, supportsImages: boolean): ResponseInputContent[] | undefined;
344
+ export declare function convertResponsesInputContent(content: string | Array<TextContent | ImageContent>, supportsImages: boolean, supportsImageDetailOriginal: boolean): ResponseInputContent[] | undefined;
345
345
  export interface BuildResponsesInputOptions<TApi extends Api> {
346
346
  model: Model<TApi>;
347
347
  context: Context;
348
348
  strictResponsesPairing: boolean;
349
+ supportsImageDetailOriginal: boolean;
349
350
  systemRole?: "system" | "developer";
350
351
  nativeHistory?: {
351
352
  replay: boolean;
@@ -357,7 +358,7 @@ export interface BuildResponsesInputOptions<TApi extends Api> {
357
358
  }
358
359
  export declare function buildResponsesInput<TApi extends Api>(options: BuildResponsesInputOptions<TApi>): ResponseInput;
359
360
  export declare function convertResponsesAssistantMessage<TApi extends Api>(assistantMsg: AssistantMessage, model: Model<TApi>, msgIndex: number, knownCallIds: Set<string>, includeThinkingSignatures?: boolean, customCallIds?: Set<string>): ResponseInput;
360
- export declare function appendResponsesToolResultMessages<TApi extends Api>(messages: ResponseInput, toolResult: ToolResultMessage, model: Model<TApi>, strictResponsesPairing: boolean, knownCallIds: ReadonlySet<string>, customCallIds?: ReadonlySet<string>): void;
361
+ export declare function appendResponsesToolResultMessages<TApi extends Api>(messages: ResponseInput, toolResult: ToolResultMessage, model: Model<TApi>, strictResponsesPairing: boolean, supportsImageDetailOriginal: boolean, knownCallIds: ReadonlySet<string>, customCallIds?: ReadonlySet<string>): void;
361
362
  /**
362
363
  * Per-block accumulation helpers shared by the two Responses decode loops —
363
364
  * {@link processResponsesStream} (generic Responses) and the Codex stream
@@ -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.8",
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.8",
42
- "@oh-my-pi/pi-utils": "16.0.8",
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"
@@ -1352,6 +1352,19 @@ export class AuthStorage {
1352
1352
  const sessionMap = this.#sessionLastCredential.get(provider) ?? new Map();
1353
1353
  sessionMap.set(sessionId, { type, index });
1354
1354
  this.#sessionLastCredential.set(provider, sessionMap);
1355
+
1356
+ try {
1357
+ const credentialId = this.#getStoredCredentials(provider)[index]?.id;
1358
+ if (credentialId !== undefined) {
1359
+ const cacheKey = `session:sticky:${provider}:${sessionId}`;
1360
+ const cacheValue = JSON.stringify({ type, index, credentialId });
1361
+ // Expires in 30 days
1362
+ const expiresAtSec = Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60;
1363
+ this.#store.setCache(cacheKey, cacheValue, expiresAtSec);
1364
+ }
1365
+ } catch (err) {
1366
+ logger.debug("Failed to write session sticky credential to persistent store cache", { err });
1367
+ }
1355
1368
  }
1356
1369
 
1357
1370
  /** Retrieves the last credential used by a session. */
@@ -1360,17 +1373,59 @@ export class AuthStorage {
1360
1373
  sessionId: string | undefined,
1361
1374
  ): { type: AuthCredential["type"]; index: number } | undefined {
1362
1375
  if (!sessionId) return undefined;
1363
- return this.#sessionLastCredential.get(provider)?.get(sessionId);
1376
+ let sessionMap = this.#sessionLastCredential.get(provider);
1377
+ if (sessionMap?.has(sessionId)) {
1378
+ return sessionMap.get(sessionId);
1379
+ }
1380
+ try {
1381
+ const cacheKey = `session:sticky:${provider}:${sessionId}`;
1382
+ const raw = this.#store.getCache(cacheKey);
1383
+ if (raw) {
1384
+ const val = JSON.parse(raw) as { type: AuthCredential["type"]; index: number; credentialId?: number };
1385
+
1386
+ if (val.credentialId !== undefined) {
1387
+ const stored = this.#getStoredCredentials(provider);
1388
+ const actualIndex = stored.findIndex(entry => entry.id === val.credentialId);
1389
+ if (actualIndex === -1 || stored[actualIndex]?.credential.type !== val.type) {
1390
+ this.#store.setCache(cacheKey, "", 0);
1391
+ return undefined;
1392
+ }
1393
+ val.index = actualIndex;
1394
+ } else {
1395
+ // Fallback: drop unsafe index-only cache rows to prevent wrong-account routing
1396
+ this.#store.setCache(cacheKey, "", 0);
1397
+ return undefined;
1398
+ }
1399
+
1400
+ if (!sessionMap) {
1401
+ sessionMap = new Map();
1402
+ this.#sessionLastCredential.set(provider, sessionMap);
1403
+ }
1404
+ const sessionVal = { type: val.type, index: val.index };
1405
+ sessionMap.set(sessionId, sessionVal);
1406
+ return sessionVal;
1407
+ }
1408
+ } catch (err) {
1409
+ logger.debug("Failed to read session sticky credential from persistent store cache", { err });
1410
+ }
1411
+ return undefined;
1364
1412
  }
1365
1413
 
1366
1414
  /** Clears the last credential used by a session for a provider. */
1367
1415
  #clearSessionCredential(provider: string, sessionId: string | undefined): void {
1368
1416
  if (!sessionId) return;
1369
1417
  const sessionMap = this.#sessionLastCredential.get(provider);
1370
- if (!sessionMap) return;
1371
- sessionMap.delete(sessionId);
1372
- if (sessionMap.size === 0) {
1373
- this.#sessionLastCredential.delete(provider);
1418
+ if (sessionMap) {
1419
+ sessionMap.delete(sessionId);
1420
+ if (sessionMap.size === 0) {
1421
+ this.#sessionLastCredential.delete(provider);
1422
+ }
1423
+ }
1424
+ try {
1425
+ const cacheKey = `session:sticky:${provider}:${sessionId}`;
1426
+ this.#store.setCache(cacheKey, "", 0);
1427
+ } catch (err) {
1428
+ logger.debug("Failed to clear session sticky credential from persistent store cache", { err });
1374
1429
  }
1375
1430
  }
1376
1431
 
@@ -1549,6 +1604,17 @@ export class AuthStorage {
1549
1604
  return rows;
1550
1605
  }
1551
1606
 
1607
+ async #upsertOAuthCredential(provider: string, credential: OAuthCredential): Promise<void> {
1608
+ const stored = this.#store.upsertAuthCredentialRemote
1609
+ ? await this.#store.upsertAuthCredentialRemote(provider, credential)
1610
+ : this.#store.upsertAuthCredentialForProvider(provider, credential);
1611
+ this.#setStoredCredentials(
1612
+ provider,
1613
+ stored.map(entry => ({ id: entry.id, credential: entry.credential })),
1614
+ );
1615
+ this.#resetProviderAssignments(provider);
1616
+ }
1617
+
1552
1618
  /**
1553
1619
  * Remove credential for a provider.
1554
1620
  */
@@ -1786,10 +1852,10 @@ export class AuthStorage {
1786
1852
  return;
1787
1853
  }
1788
1854
  const newCredential: OAuthCredential = { type: "oauth", ...result };
1789
- // Use set() instead of #upsertOAuthCredential to replace ALL existing credentials
1790
- // (including legacy api_key rows from older versions) with the new OAuth credential.
1791
- // This ensures getApiKey() doesn't match an old api_key row before the new OAuth row.
1792
- await this.set(def.storeCredentialsAs ?? provider, newCredential);
1855
+ // Use #upsertOAuthCredential to upsert the new credential.
1856
+ // Any legacy api_key rows from older versions will be cleaned up so they do not
1857
+ // shadow the new OAuth row, while preserving other active OAuth credentials.
1858
+ await this.#upsertOAuthCredential(def.storeCredentialsAs ?? provider, newCredential);
1793
1859
  }
1794
1860
 
1795
1861
  /**
@@ -4916,6 +4982,14 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
4916
4982
  identityKey: resolveRowCredentialIdentityKey(providerName, row),
4917
4983
  }));
4918
4984
 
4985
+ if (item.type === "oauth") {
4986
+ for (const row of existing) {
4987
+ if (row.credential && row.credential.type === "api_key") {
4988
+ this.#deleteStmt.run("replaced by oauth login", row.id);
4989
+ }
4990
+ }
4991
+ }
4992
+
4919
4993
  let targetId: number | null = null;
4920
4994
  for (const row of existing) {
4921
4995
  if (!matchesReplacementCredential(providerName, row.credential, row.identityKey, item)) continue;
@@ -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: ["<tool_response>"],
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 is a `<call:NAME>…</call:NAME>` block (or self-closing `<call:NAME …/>`) written as plain assistant text; arguments are given as tag attributes, child elements, or a verbatim inline body.
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
- ```text
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
- <call:configure>
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
- A single string argument can fill the body directly:
11
+ Call with a verbatim body everything between `«` and `»` is taken literally, no quoting or escaping:
22
12
 
23
13
  ```text
24
- <call:edit>
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
- </call:edit>
21
+ »
29
22
  ```
30
23
 
31
- Tool results arrive as response blocks, read in call order:
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
- <tool_response>
35
- verbatim tool result
36
- </tool_response>
34
+ ¤
35
+ brief reasoning
36
+ ¤
37
37
  ```
38
38
 
39
- Private reasoning goes in a `<thinking>…</thinking>` block before your calls:
39
+ Tool results arrive in `‡‡…‡‡` blocks, read in call order:
40
40
 
41
41
  ```text
42
- <thinking>
43
- brief reasoning
44
- </thinking>
42
+ ‡‡
43
+ verbatim tool result
44
+ ‡‡
45
45
  ```
46
46
 
47
47
  ## Rules
48
48
 
49
- - `NAME` must match a listed function; never wrap calls in JSON or fences.
50
- - Use attributes only for top-level scalars; put objects, arrays, and long strings in child elements.
51
- - Strings are verbatim (no quotes, no entity escaping); numbers, booleans, and null are JSON literals.
52
- - An object opens a child block whose scalar subfields may also be attributes; an array repeats its element once per item.
53
- - The inline body fills the first unset string-typed parameter and may contain any raw text except `</call:NAME>`.
54
- - Emit parallel calls as consecutive blocks. NEVER invent call ids; results are positional.
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.