@oh-my-pi/pi-ai 16.0.8 → 16.0.9
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 +9 -0
- package/dist/types/providers/openai-shared.d.ts +3 -2
- package/package.json +3 -3
- package/src/auth-storage.ts +83 -9
- package/src/providers/azure-openai-responses.ts +1 -0
- package/src/providers/openai-codex-responses.ts +13 -2
- package/src/providers/openai-responses.ts +1 -0
- package/src/providers/openai-shared.ts +25 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.0.9] - 2026-06-18
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed OAuth login replacing all other active accounts for the same provider, allowing multiple OAuth accounts to coexist concurrently.
|
|
10
|
+
- Fixed legacy `api_key` credentials not being replaced/disabled atomically upon upgrading to OAuth login.
|
|
11
|
+
- 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.
|
|
12
|
+
- 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))
|
|
13
|
+
|
|
5
14
|
## [16.0.8] - 2026-06-18
|
|
6
15
|
|
|
7
16
|
### 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
|
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.9",
|
|
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.9",
|
|
42
|
+
"@oh-my-pi/pi-utils": "16.0.9",
|
|
43
43
|
"arktype": "^2.2.0",
|
|
44
44
|
"partial-json": "^0.1.7",
|
|
45
45
|
"zod": "^4"
|
package/src/auth-storage.ts
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
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
|
|
1790
|
-
//
|
|
1791
|
-
//
|
|
1792
|
-
await this
|
|
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;
|
|
@@ -3253,7 +3253,15 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
|
|
3253
3253
|
}
|
|
3254
3254
|
|
|
3255
3255
|
if (msg.role === "toolResult") {
|
|
3256
|
-
appendResponsesToolResultMessages(
|
|
3256
|
+
appendResponsesToolResultMessages(
|
|
3257
|
+
messages,
|
|
3258
|
+
msg,
|
|
3259
|
+
model,
|
|
3260
|
+
false,
|
|
3261
|
+
model.compat.supportsImageDetailOriginal,
|
|
3262
|
+
knownCallIds,
|
|
3263
|
+
customCallIds,
|
|
3264
|
+
);
|
|
3257
3265
|
}
|
|
3258
3266
|
|
|
3259
3267
|
msgIndex += 1;
|
|
@@ -3271,7 +3279,10 @@ function normalizeInputMessageContent(
|
|
|
3271
3279
|
return [{ type: "input_text", text: content.toWellFormed() }];
|
|
3272
3280
|
}
|
|
3273
3281
|
|
|
3274
|
-
return
|
|
3282
|
+
return (
|
|
3283
|
+
convertResponsesInputContent(content, model.input.includes("image"), model.compat.supportsImageDetailOriginal) ??
|
|
3284
|
+
[]
|
|
3285
|
+
);
|
|
3275
3286
|
}
|
|
3276
3287
|
|
|
3277
3288
|
/** @internal Exported for tests. */
|
|
@@ -687,6 +687,7 @@ export function buildParams(
|
|
|
687
687
|
model,
|
|
688
688
|
context,
|
|
689
689
|
strictResponsesPairing,
|
|
690
|
+
supportsImageDetailOriginal: model.compat.supportsImageDetailOriginal,
|
|
690
691
|
nativeHistory: {
|
|
691
692
|
replay: shouldReplayNativeHistory,
|
|
692
693
|
filterReasoning: policy.reasoning.filterReasoningHistory,
|
|
@@ -1178,9 +1178,24 @@ export function repairOrphanResponsesToolCalls(input: ResponseInput): ResponseIn
|
|
|
1178
1178
|
return repaired;
|
|
1179
1179
|
}
|
|
1180
1180
|
|
|
1181
|
+
/**
|
|
1182
|
+
* Some Responses backends (notably GitHub Copilot) reject the OpenAI image
|
|
1183
|
+
* `detail: "original"` value with a 400. When the model does not advertise
|
|
1184
|
+
* support for it, degrade `"original"` to `"auto"` so the request still goes
|
|
1185
|
+
* through with the closest valid fidelity instead of failing outright. See #2822.
|
|
1186
|
+
*/
|
|
1187
|
+
function clampResponsesImageDetail(
|
|
1188
|
+
detail: ImageContent["detail"],
|
|
1189
|
+
supportsImageDetailOriginal: boolean,
|
|
1190
|
+
): ResponseInputImage["detail"] {
|
|
1191
|
+
const resolved = detail ?? "auto";
|
|
1192
|
+
return resolved === "original" && !supportsImageDetailOriginal ? "auto" : resolved;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1181
1195
|
export function convertResponsesInputContent(
|
|
1182
1196
|
content: string | Array<TextContent | ImageContent>,
|
|
1183
1197
|
supportsImages: boolean,
|
|
1198
|
+
supportsImageDetailOriginal: boolean,
|
|
1184
1199
|
): ResponseInputContent[] | undefined {
|
|
1185
1200
|
if (typeof content === "string") {
|
|
1186
1201
|
if (content.trim().length === 0) return undefined;
|
|
@@ -1200,7 +1215,7 @@ export function convertResponsesInputContent(
|
|
|
1200
1215
|
for (const item of imageBlocks) {
|
|
1201
1216
|
normalizedContent.push({
|
|
1202
1217
|
type: "input_image",
|
|
1203
|
-
detail: item.detail
|
|
1218
|
+
detail: clampResponsesImageDetail(item.detail, supportsImageDetailOriginal),
|
|
1204
1219
|
image_url: `data:${item.mimeType};base64,${item.data}`,
|
|
1205
1220
|
} satisfies ResponseInputImage);
|
|
1206
1221
|
}
|
|
@@ -1217,6 +1232,7 @@ export interface BuildResponsesInputOptions<TApi extends Api> {
|
|
|
1217
1232
|
model: Model<TApi>;
|
|
1218
1233
|
context: Context;
|
|
1219
1234
|
strictResponsesPairing: boolean;
|
|
1235
|
+
supportsImageDetailOriginal: boolean;
|
|
1220
1236
|
systemRole?: "system" | "developer";
|
|
1221
1237
|
nativeHistory?: {
|
|
1222
1238
|
replay: boolean;
|
|
@@ -1267,7 +1283,11 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
|
|
|
1267
1283
|
msgIndex++;
|
|
1268
1284
|
continue;
|
|
1269
1285
|
}
|
|
1270
|
-
const content = convertResponsesInputContent(
|
|
1286
|
+
const content = convertResponsesInputContent(
|
|
1287
|
+
msg.content,
|
|
1288
|
+
options.model.input.includes("image"),
|
|
1289
|
+
options.supportsImageDetailOriginal,
|
|
1290
|
+
);
|
|
1271
1291
|
if (!content) continue;
|
|
1272
1292
|
messages.push({
|
|
1273
1293
|
role: "user",
|
|
@@ -1318,6 +1338,7 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
|
|
|
1318
1338
|
msg,
|
|
1319
1339
|
options.model,
|
|
1320
1340
|
options.strictResponsesPairing,
|
|
1341
|
+
options.supportsImageDetailOriginal,
|
|
1321
1342
|
knownCallIds,
|
|
1322
1343
|
customCallIds,
|
|
1323
1344
|
);
|
|
@@ -1419,6 +1440,7 @@ export function appendResponsesToolResultMessages<TApi extends Api>(
|
|
|
1419
1440
|
toolResult: ToolResultMessage,
|
|
1420
1441
|
model: Model<TApi>,
|
|
1421
1442
|
strictResponsesPairing: boolean,
|
|
1443
|
+
supportsImageDetailOriginal: boolean,
|
|
1422
1444
|
knownCallIds: ReadonlySet<string>,
|
|
1423
1445
|
customCallIds?: ReadonlySet<string>,
|
|
1424
1446
|
): void {
|
|
@@ -1475,7 +1497,7 @@ export function appendResponsesToolResultMessages<TApi extends Api>(
|
|
|
1475
1497
|
if (block.type === "image") {
|
|
1476
1498
|
contentParts.push({
|
|
1477
1499
|
type: "input_image",
|
|
1478
|
-
detail: block.detail
|
|
1500
|
+
detail: clampResponsesImageDetail(block.detail, supportsImageDetailOriginal),
|
|
1479
1501
|
image_url: `data:${block.mimeType};base64,${block.data}`,
|
|
1480
1502
|
} satisfies ResponseInputImage);
|
|
1481
1503
|
}
|