@oh-my-pi/pi-ai 18.14.2 → 18.15.0
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/auth/types.d.ts +11 -5
- package/dist/types/providers/anthropic.d.ts +5 -0
- package/dist/types/providers/google-auth.d.ts +2 -3
- package/dist/types/providers/openai-codex/request-transformer.d.ts +1 -0
- package/dist/types/types.d.ts +4 -2
- package/dist/types/utils.d.ts +5 -0
- package/package.json +6 -6
- package/src/auth/refresh.ts +5 -2
- package/src/auth/types.ts +11 -5
- package/src/auth/usage.ts +1 -0
- package/src/auth-broker/client.ts +1 -1
- package/src/auth-broker/server.ts +7 -2
- package/src/providers/anthropic.ts +13 -0
- package/src/providers/google-auth.ts +57 -13
- package/src/providers/openai-codex/request-transformer.ts +8 -8
- package/src/types.ts +9 -3
- package/src/utils.ts +15 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.15.0] - 2026-11-01
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed Codex conversation replay failing with an unknown `internal_chat_message_metadata_passthrough.content_token_count` parameter by omitting backend-internal response metadata from replayed input ([#17590](https://github.com/can1357/oh-my-pi/pull/17590), [#17597](https://github.com/can1357/oh-my-pi/pull/17597) by [@briankeefe](https://github.com/briankeefe)).
|
|
10
|
+
- Fixed `service_tier: "ultrafast"` being dropped for OpenAI-family models on custom providers even when the model's `serviceTiers` lists it ([#17577](https://github.com/can1357/oh-my-pi/pull/17577) by [@jakehemmerle](https://github.com/jakehemmerle))
|
|
11
|
+
- Fixed Google Vertex requests with Workforce Identity Federation ADC failing token refresh against the wrong endpoint ([#17447](https://github.com/can1357/oh-my-pi/issues/17447)).
|
|
12
|
+
- Fixed Anthropic (and other rotating-token) OAuth accounts being signed out automatically when a background usage poll's token refresh got `invalid_grant`, including refreshes a client delegates to an auth broker; usage polling no longer disables credentials, so only a real request's refresh can do that ([#17468](https://github.com/can1357/oh-my-pi/issues/17468))
|
|
13
|
+
|
|
5
14
|
## [18.14.2] - 2026-10-31
|
|
6
15
|
|
|
7
16
|
### Breaking Changes
|
|
@@ -279,8 +279,13 @@ export interface CredentialDisabledEvent {
|
|
|
279
279
|
orgId?: string;
|
|
280
280
|
orgName?: string;
|
|
281
281
|
}
|
|
282
|
-
/**
|
|
283
|
-
|
|
282
|
+
/**
|
|
283
|
+
* Why an OAuth refresh was requested.
|
|
284
|
+
* - `auth-recovery`: a provider rejected the bearer with a 401.
|
|
285
|
+
* - `usage-probe`: a background usage poll; advisory, so a definitive refresh
|
|
286
|
+
* failure never disables the credential (an interactive request decides).
|
|
287
|
+
*/
|
|
288
|
+
export type OAuthRefreshReason = "auth-recovery" | "usage-probe";
|
|
284
289
|
/** Configuration supplied when constructing credential storage. */
|
|
285
290
|
export type AuthStorageOptions = {
|
|
286
291
|
usageProviderResolver?: (provider: Provider) => UsageProvider | undefined;
|
|
@@ -979,9 +984,10 @@ export interface OAuthRefreshByIdOptions {
|
|
|
979
984
|
*/
|
|
980
985
|
reuseRecentMint?: boolean;
|
|
981
986
|
/**
|
|
982
|
-
*
|
|
983
|
-
* broker), which may then return a token it minted in the
|
|
984
|
-
*
|
|
987
|
+
* Why the refresh was requested. `auth-recovery` is forwarded to a delegated
|
|
988
|
+
* refresh (an auth broker), which may then return a token it minted in the
|
|
989
|
+
* last five minutes; local reuse still requires {@link reuseRecentMint}.
|
|
990
|
+
* `usage-probe` never disables the row on a definitive refresh failure.
|
|
985
991
|
*/
|
|
986
992
|
reason?: OAuthRefreshReason;
|
|
987
993
|
/** The access token a provider 401 rejected; see {@link AuthApiKeyOptions.rejectedAccessToken}. */
|
|
@@ -17,6 +17,11 @@ export * from "./claude-code-fingerprint.js";
|
|
|
17
17
|
* pass through untouched, so installing it on every OAuth flow is safe.
|
|
18
18
|
*/
|
|
19
19
|
export declare function wrapFetchForCch(base: FetchImpl): FetchImpl;
|
|
20
|
+
/**
|
|
21
|
+
* Whether a request to `model` sends the tool `name` for the host to run itself, never dispatching the call to the
|
|
22
|
+
* client: the same decision `encodeAnthropicToolName` makes on the wire.
|
|
23
|
+
*/
|
|
24
|
+
export declare function anthropicHostRunsTool(model: Model<"anthropic-messages">, name: string, headers?: Record<string, string>): boolean;
|
|
20
25
|
export type AnthropicEffort = AnthropicOutputEffort | "adaptive";
|
|
21
26
|
export type AnthropicThinkingDisplay = "summarized" | "omitted";
|
|
22
27
|
export interface AnthropicOptions extends StreamOptions {
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Replaces `google-auth-library` with a direct WebCrypto + REST implementation.
|
|
5
5
|
* Sources, in priority order:
|
|
6
|
-
* 1. `GOOGLE_APPLICATION_CREDENTIALS` env →
|
|
7
|
-
* or `type: "authorized_user"` (refresh-token exchange).
|
|
6
|
+
* 1. `GOOGLE_APPLICATION_CREDENTIALS` env → service account, user, or workforce ADC file.
|
|
8
7
|
* 2. gcloud user ADC: `%APPDATA%\gcloud\application_default_credentials.json` on Windows,
|
|
9
|
-
* `~/.config/gcloud/application_default_credentials.json` elsewhere
|
|
8
|
+
* `~/.config/gcloud/application_default_credentials.json` elsewhere.
|
|
10
9
|
* 3. GCE / Cloud Run metadata server (`metadata.google.internal`).
|
|
11
10
|
*
|
|
12
11
|
* Tokens are cached per source key and refreshed `GOOGLE_VERTEX_REFRESH_SKEW_MS` before expiry
|
package/dist/types/types.d.ts
CHANGED
|
@@ -144,8 +144,10 @@ export declare function resolveModelServiceTier(tiers: ServiceTierByFamily | nul
|
|
|
144
144
|
* `service_tier_for_request`); an empty or missing list counts as "not
|
|
145
145
|
* reported" — accounts whose `/models` lists no tiers keep `/fast` — so the
|
|
146
146
|
* provider-level answer stands. `flex` and `default` are never gated.
|
|
147
|
-
* First-party OpenAI takes `ultrafast` as-is.
|
|
148
|
-
*
|
|
147
|
+
* First-party OpenAI takes `ultrafast` as-is. An OpenAI-family relay on the
|
|
148
|
+
* Responses/Completions APIs gets `ultrafast` only when its discovered
|
|
149
|
+
* `service_tiers` lists it. A bare provider string cannot carry the list, so
|
|
150
|
+
* it answers for the provider alone.
|
|
149
151
|
*/
|
|
150
152
|
export declare function shouldSendServiceTier(serviceTier: ServiceTier | null | undefined, target: Provider | ServiceTierModel | undefined): boolean;
|
|
151
153
|
/**
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -34,6 +34,11 @@ export declare function stripOpenAIResponsesOutputOnlyStatusesForReplay<TItem ex
|
|
|
34
34
|
type?: unknown;
|
|
35
35
|
status?: unknown;
|
|
36
36
|
}>(items: TItem[]): TItem[];
|
|
37
|
+
/** Remove Codex-rejected backend metadata; preserve clean-item identity by default, or copy while keeping/dropping id. */
|
|
38
|
+
export declare function stripOpenAIResponsesInternalMetadataForReplay<TItem extends {
|
|
39
|
+
id?: unknown;
|
|
40
|
+
internal_chat_message_metadata_passthrough?: unknown;
|
|
41
|
+
}>(item: TItem, mode?: "preserve" | "copy" | "drop-id"): TItem;
|
|
37
42
|
/** Clamp replayed images without changing item identities or other native replay fields. */
|
|
38
43
|
export declare function clampOpenAIResponsesImageDetailForReplay<TItem extends {
|
|
39
44
|
type?: unknown;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.15.0",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -155,11 +155,11 @@
|
|
|
155
155
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
156
156
|
},
|
|
157
157
|
"dependencies": {
|
|
158
|
-
"@oh-my-pi/omptype": "18.
|
|
159
|
-
"@oh-my-pi/pi-catalog": "18.
|
|
160
|
-
"@oh-my-pi/pi-natives": "18.
|
|
161
|
-
"@oh-my-pi/pi-utils": "18.
|
|
162
|
-
"@oh-my-pi/pi-wire": "18.
|
|
158
|
+
"@oh-my-pi/omptype": "18.15.0",
|
|
159
|
+
"@oh-my-pi/pi-catalog": "18.15.0",
|
|
160
|
+
"@oh-my-pi/pi-natives": "18.15.0",
|
|
161
|
+
"@oh-my-pi/pi-utils": "18.15.0",
|
|
162
|
+
"@oh-my-pi/pi-wire": "18.15.0"
|
|
163
163
|
},
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@types/bun": "^1.3.14"
|
package/src/auth/refresh.ts
CHANGED
|
@@ -1006,7 +1006,9 @@ export class OAuthRefresher {
|
|
|
1006
1006
|
// `refreshStored` owns the caller budget and the exchange backstop.
|
|
1007
1007
|
refresh: (current, refreshSignal) =>
|
|
1008
1008
|
this.#requestOAuthCredentialRefresh(provider, current, credentialId, refreshSignal, reason, undefined),
|
|
1009
|
-
|
|
1009
|
+
// A usage probe is advisory: leave a dead grant for an interactive request to disable.
|
|
1010
|
+
isDefinitiveFailure:
|
|
1011
|
+
reason === "usage-probe" ? undefined : error => AIError.isDefinitiveOAuthFailure(String(error)),
|
|
1010
1012
|
disabledCause: error => `oauth refresh failed: ${String(error)}`,
|
|
1011
1013
|
});
|
|
1012
1014
|
if (result.credential) {
|
|
@@ -1199,7 +1201,8 @@ export class OAuthRefresher {
|
|
|
1199
1201
|
// A definitively-dead grant tears the row down here, where the
|
|
1200
1202
|
// attempted credential is known. CAS on the persisted credential so a
|
|
1201
1203
|
// peer/login rotation in flight leaves the freshly-rotated row intact.
|
|
1202
|
-
|
|
1204
|
+
// A usage probe is advisory: leave a dead grant for an interactive request to disable.
|
|
1205
|
+
if (reason !== "usage-probe" && AIError.isDefinitiveOAuthFailure(String(error))) {
|
|
1203
1206
|
// CAS-loss (false) means a peer/login rotated the row mid-refresh, so
|
|
1204
1207
|
// our #data copy is stale — reload so the next caller serves the
|
|
1205
1208
|
// freshly-rotated credential rather than the dead token we attempted.
|
package/src/auth/types.ts
CHANGED
|
@@ -329,8 +329,13 @@ export interface CredentialDisabledEvent {
|
|
|
329
329
|
orgName?: string;
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
-
/**
|
|
333
|
-
|
|
332
|
+
/**
|
|
333
|
+
* Why an OAuth refresh was requested.
|
|
334
|
+
* - `auth-recovery`: a provider rejected the bearer with a 401.
|
|
335
|
+
* - `usage-probe`: a background usage poll; advisory, so a definitive refresh
|
|
336
|
+
* failure never disables the credential (an interactive request decides).
|
|
337
|
+
*/
|
|
338
|
+
export type OAuthRefreshReason = "auth-recovery" | "usage-probe";
|
|
334
339
|
|
|
335
340
|
/** Configuration supplied when constructing credential storage. */
|
|
336
341
|
export type AuthStorageOptions = {
|
|
@@ -1064,9 +1069,10 @@ export interface OAuthRefreshByIdOptions {
|
|
|
1064
1069
|
*/
|
|
1065
1070
|
reuseRecentMint?: boolean;
|
|
1066
1071
|
/**
|
|
1067
|
-
*
|
|
1068
|
-
* broker), which may then return a token it minted in the
|
|
1069
|
-
*
|
|
1072
|
+
* Why the refresh was requested. `auth-recovery` is forwarded to a delegated
|
|
1073
|
+
* refresh (an auth broker), which may then return a token it minted in the
|
|
1074
|
+
* last five minutes; local reuse still requires {@link reuseRecentMint}.
|
|
1075
|
+
* `usage-probe` never disables the row on a definitive refresh failure.
|
|
1070
1076
|
*/
|
|
1071
1077
|
reason?: OAuthRefreshReason;
|
|
1072
1078
|
/** The access token a provider 401 rejected; see {@link AuthApiKeyOptions.rejectedAccessToken}. */
|
package/src/auth/usage.ts
CHANGED
|
@@ -232,6 +232,7 @@ export class UsageService implements UsageApi {
|
|
|
232
232
|
refreshableCredential,
|
|
233
233
|
refreshableCredentialId,
|
|
234
234
|
timeoutSignal,
|
|
235
|
+
"usage-probe",
|
|
235
236
|
);
|
|
236
237
|
const refreshedCredential = mergeRefreshedUsageCredential(request.credential, refreshed);
|
|
237
238
|
this.persistRefreshedCredential(
|
|
@@ -327,7 +327,7 @@ export class AuthBrokerClient {
|
|
|
327
327
|
signal?: AbortSignal,
|
|
328
328
|
reason?: OAuthRefreshReason,
|
|
329
329
|
): Promise<CredentialRefreshResponse> {
|
|
330
|
-
const suffix = reason
|
|
330
|
+
const suffix = reason ? `?reason=${reason}` : "";
|
|
331
331
|
return this.#request<CredentialRefreshResponse>("POST", `/v1/credential/${id}/refresh${suffix}`, {
|
|
332
332
|
schema: "credentialRefreshResponseSchema",
|
|
333
333
|
signal,
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
|
|
54
54
|
const DEFAULT_EXTERNAL_CHANGE_POLL_MS = 250;
|
|
55
55
|
const AUTH_RECOVERY_REFRESH_OPTIONS = { reuseRecentMint: true } as const;
|
|
56
|
+
const USAGE_PROBE_REFRESH_OPTIONS = { reason: "usage-probe" } as const;
|
|
56
57
|
const TEXT_ENCODER = new TextEncoder();
|
|
57
58
|
const SSE_KEEPALIVE = TEXT_ENCODER.encode(": keepalive\n\n");
|
|
58
59
|
|
|
@@ -935,12 +936,16 @@ export function startAuthBroker(opts: AuthBrokerServerOptions): AuthBrokerServer
|
|
|
935
936
|
const refreshMatch = req.method === "POST" ? pathname.match(REFRESH_ROUTE) : null;
|
|
936
937
|
if (refreshMatch) {
|
|
937
938
|
const id = Number.parseInt(refreshMatch[1], 10);
|
|
938
|
-
const
|
|
939
|
+
const reason = url.searchParams.get("reason");
|
|
939
940
|
try {
|
|
940
941
|
const entry = await opts.storage.oauth.refresh(
|
|
941
942
|
id,
|
|
942
943
|
req.signal,
|
|
943
|
-
|
|
944
|
+
reason === "auth-recovery"
|
|
945
|
+
? AUTH_RECOVERY_REFRESH_OPTIONS
|
|
946
|
+
: reason === "usage-probe"
|
|
947
|
+
? USAGE_PROBE_REFRESH_OPTIONS
|
|
948
|
+
: undefined,
|
|
944
949
|
);
|
|
945
950
|
const body: CredentialRefreshResponse = { entry };
|
|
946
951
|
logger.info("auth-broker credential refreshed", {
|
|
@@ -652,6 +652,19 @@ function getUmansWebSearchHeader(
|
|
|
652
652
|
return provider ? { [UMANS_WEBSEARCH_PROVIDER_HEADER]: provider } : undefined;
|
|
653
653
|
}
|
|
654
654
|
|
|
655
|
+
/**
|
|
656
|
+
* Whether a request to `model` sends the tool `name` for the host to run itself, never dispatching the call to the
|
|
657
|
+
* client: the same decision `encodeAnthropicToolName` makes on the wire.
|
|
658
|
+
*/
|
|
659
|
+
export function anthropicHostRunsTool(
|
|
660
|
+
model: Model<"anthropic-messages">,
|
|
661
|
+
name: string,
|
|
662
|
+
headers?: Record<string, string>,
|
|
663
|
+
): boolean {
|
|
664
|
+
const gatewayWebSearch = getUmansWebSearchHeader(model, mergeHeaders(model.headers, headers)) !== undefined;
|
|
665
|
+
return shouldUseUmansGatewayWebSearch(name, gatewayWebSearch);
|
|
666
|
+
}
|
|
667
|
+
|
|
655
668
|
function shouldUseUmansGatewayWebSearch(name: string, enabled: boolean): boolean {
|
|
656
669
|
return enabled && name.toLowerCase() === UMANS_WEBSEARCH_TOOL_NAME;
|
|
657
670
|
}
|
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Replaces `google-auth-library` with a direct WebCrypto + REST implementation.
|
|
5
5
|
* Sources, in priority order:
|
|
6
|
-
* 1. `GOOGLE_APPLICATION_CREDENTIALS` env →
|
|
7
|
-
* or `type: "authorized_user"` (refresh-token exchange).
|
|
6
|
+
* 1. `GOOGLE_APPLICATION_CREDENTIALS` env → service account, user, or workforce ADC file.
|
|
8
7
|
* 2. gcloud user ADC: `%APPDATA%\gcloud\application_default_credentials.json` on Windows,
|
|
9
|
-
* `~/.config/gcloud/application_default_credentials.json` elsewhere
|
|
8
|
+
* `~/.config/gcloud/application_default_credentials.json` elsewhere.
|
|
10
9
|
* 3. GCE / Cloud Run metadata server (`metadata.google.internal`).
|
|
11
10
|
*
|
|
12
11
|
* Tokens are cached per source key and refreshed `GOOGLE_VERTEX_REFRESH_SKEW_MS` before expiry
|
|
@@ -45,6 +44,14 @@ interface AuthorizedUserCredentials {
|
|
|
45
44
|
refresh_token: string;
|
|
46
45
|
}
|
|
47
46
|
|
|
47
|
+
interface ExternalAccountAuthorizedUserCredentials {
|
|
48
|
+
type: "external_account_authorized_user";
|
|
49
|
+
client_id: string;
|
|
50
|
+
client_secret: string;
|
|
51
|
+
refresh_token: string;
|
|
52
|
+
token_url: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
interface ImpersonatedServiceAccountCredentials {
|
|
49
56
|
type: "impersonated_service_account";
|
|
50
57
|
service_account_impersonation_url: string;
|
|
@@ -52,16 +59,28 @@ interface ImpersonatedServiceAccountCredentials {
|
|
|
52
59
|
delegates?: string[];
|
|
53
60
|
}
|
|
54
61
|
|
|
55
|
-
type AdcFileCredentials =
|
|
62
|
+
type AdcFileCredentials =
|
|
63
|
+
| ServiceAccountCredentials
|
|
64
|
+
| AuthorizedUserCredentials
|
|
65
|
+
| ExternalAccountAuthorizedUserCredentials
|
|
66
|
+
| ImpersonatedServiceAccountCredentials;
|
|
56
67
|
|
|
57
68
|
interface TokenResponse {
|
|
58
69
|
access_token: string;
|
|
59
70
|
expires_in: number;
|
|
60
71
|
token_type?: string;
|
|
72
|
+
/** Replacement refresh token; STS may rotate workforce refresh tokens on exchange. */
|
|
73
|
+
refresh_token?: string;
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
const tokenCache = new Map<string, CachedToken>();
|
|
64
77
|
const inflight = new Map<string, Promise<string>>();
|
|
78
|
+
/**
|
|
79
|
+
* Latest workforce refresh token, keyed by the token stored in the ADC file. The file is never
|
|
80
|
+
* rewritten here when STS rotates the token, so later refreshes must use the replacement;
|
|
81
|
+
* a re-login writes a new file token and bypasses the stale entry.
|
|
82
|
+
*/
|
|
83
|
+
const rotatedRefreshTokens = new Map<string, string>();
|
|
65
84
|
|
|
66
85
|
function getRefreshSkewMs(): number {
|
|
67
86
|
return $envpos("GOOGLE_VERTEX_REFRESH_SKEW_MS", 60_000);
|
|
@@ -168,6 +187,19 @@ async function exchangeRefreshToken(
|
|
|
168
187
|
return postForToken(OAUTH_TOKEN_URL, body, signal, fetchImpl);
|
|
169
188
|
}
|
|
170
189
|
|
|
190
|
+
async function exchangeWorkforceRefreshToken(
|
|
191
|
+
creds: ExternalAccountAuthorizedUserCredentials,
|
|
192
|
+
signal: AbortSignal | undefined,
|
|
193
|
+
fetchImpl: FetchImpl,
|
|
194
|
+
): Promise<TokenResponse> {
|
|
195
|
+
const refreshToken = rotatedRefreshTokens.get(creds.refresh_token) ?? creds.refresh_token;
|
|
196
|
+
const body = new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken });
|
|
197
|
+
const authorization = `Basic ${Buffer.from(`${creds.client_id}:${creds.client_secret}`, "utf8").toString("base64")}`;
|
|
198
|
+
const token = await postForToken(creds.token_url, body, signal, fetchImpl, { Authorization: authorization });
|
|
199
|
+
if (token.refresh_token) rotatedRefreshTokens.set(creds.refresh_token, token.refresh_token);
|
|
200
|
+
return token;
|
|
201
|
+
}
|
|
202
|
+
|
|
171
203
|
async function fetchMetadataToken(
|
|
172
204
|
signal: AbortSignal | undefined,
|
|
173
205
|
fetchImpl: FetchImpl,
|
|
@@ -192,10 +224,11 @@ async function postForToken(
|
|
|
192
224
|
body: URLSearchParams,
|
|
193
225
|
signal: AbortSignal | undefined,
|
|
194
226
|
fetchImpl: FetchImpl,
|
|
227
|
+
extraHeaders?: Record<string, string>,
|
|
195
228
|
): Promise<TokenResponse> {
|
|
196
229
|
const response = await fetchImpl(url, {
|
|
197
230
|
method: "POST",
|
|
198
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
231
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", ...extraHeaders },
|
|
199
232
|
body: body.toString(),
|
|
200
233
|
signal,
|
|
201
234
|
});
|
|
@@ -217,6 +250,7 @@ async function resolveAccessTokenUncached(
|
|
|
217
250
|
const adc = await loadAdcCredentials();
|
|
218
251
|
if (adc) {
|
|
219
252
|
const creds = adc.creds;
|
|
253
|
+
const credentialType: string = creds.type;
|
|
220
254
|
let token: TokenResponse;
|
|
221
255
|
|
|
222
256
|
if (creds.type === "impersonated_service_account") {
|
|
@@ -228,10 +262,16 @@ async function resolveAccessTokenUncached(
|
|
|
228
262
|
throw new RangeError(`Cannot extract target principal from ${creds.service_account_impersonation_url}`);
|
|
229
263
|
}
|
|
230
264
|
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
265
|
+
const sourceCredentials = creds.source_credentials;
|
|
266
|
+
const sourceType: string = sourceCredentials.type;
|
|
267
|
+
let sourceToken: TokenResponse;
|
|
268
|
+
if (sourceCredentials.type === "service_account") {
|
|
269
|
+
sourceToken = await exchangeJwtForToken(sourceCredentials, signal, fetchImpl);
|
|
270
|
+
} else if (sourceCredentials.type === "authorized_user") {
|
|
271
|
+
sourceToken = await exchangeRefreshToken(sourceCredentials, signal, fetchImpl);
|
|
272
|
+
} else {
|
|
273
|
+
throw new AIError.ConfigurationError(`Unsupported ADC credential type: ${sourceType}`);
|
|
274
|
+
}
|
|
235
275
|
|
|
236
276
|
const response = await fetchImpl(
|
|
237
277
|
`https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${targetPrincipal}:generateAccessToken`,
|
|
@@ -260,11 +300,14 @@ async function resolveAccessTokenUncached(
|
|
|
260
300
|
const data = (await response.json()) as { accessToken: string; expireTime: string };
|
|
261
301
|
const expiresIn = Math.max(0, Math.floor((new Date(data.expireTime).getTime() - Date.now()) / 1000));
|
|
262
302
|
token = { access_token: data.accessToken, expires_in: expiresIn, token_type: "Bearer" };
|
|
303
|
+
} else if (creds.type === "service_account") {
|
|
304
|
+
token = await exchangeJwtForToken(creds, signal, fetchImpl);
|
|
305
|
+
} else if (creds.type === "authorized_user") {
|
|
306
|
+
token = await exchangeRefreshToken(creds, signal, fetchImpl);
|
|
307
|
+
} else if (creds.type === "external_account_authorized_user") {
|
|
308
|
+
token = await exchangeWorkforceRefreshToken(creds, signal, fetchImpl);
|
|
263
309
|
} else {
|
|
264
|
-
|
|
265
|
-
creds.type === "service_account"
|
|
266
|
-
? await exchangeJwtForToken(creds, signal, fetchImpl)
|
|
267
|
-
: await exchangeRefreshToken(creds, signal, fetchImpl);
|
|
310
|
+
throw new AIError.ConfigurationError(`Unsupported ADC credential type: ${credentialType}`);
|
|
268
311
|
}
|
|
269
312
|
return { source: adc.source, token };
|
|
270
313
|
}
|
|
@@ -334,4 +377,5 @@ export async function getVertexAccessToken(options?: { signal?: AbortSignal; fet
|
|
|
334
377
|
export function __resetVertexTokenCache(): void {
|
|
335
378
|
tokenCache.clear();
|
|
336
379
|
inflight.clear();
|
|
380
|
+
rotatedRefreshTokens.clear();
|
|
337
381
|
}
|
|
@@ -2,6 +2,7 @@ import { Effort } from "@oh-my-pi/pi-catalog/effort";
|
|
|
2
2
|
import { requireSupportedEffort } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
3
3
|
import { $env } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import type { Model } from "../../types";
|
|
5
|
+
import { stripOpenAIResponsesInternalMetadataForReplay } from "../../utils";
|
|
5
6
|
import { mapOpenAIReasoningEffort } from "../openai-shared";
|
|
6
7
|
|
|
7
8
|
/** Reasoning replay scope for the Codex Responses API (`reasoning.context`). */
|
|
@@ -50,6 +51,7 @@ export interface InputItem {
|
|
|
50
51
|
type?: string | null;
|
|
51
52
|
role?: string;
|
|
52
53
|
content?: unknown;
|
|
54
|
+
internal_chat_message_metadata_passthrough?: unknown;
|
|
53
55
|
call_id?: string | null;
|
|
54
56
|
name?: string;
|
|
55
57
|
output?: unknown;
|
|
@@ -179,14 +181,12 @@ function filterInput(input: InputItem[] | undefined): InputItem[] | undefined {
|
|
|
179
181
|
|
|
180
182
|
return input
|
|
181
183
|
.filter(item => item.type !== "item_reference")
|
|
182
|
-
.map(item =>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return item;
|
|
189
|
-
});
|
|
184
|
+
.map(item =>
|
|
185
|
+
stripOpenAIResponsesInternalMetadataForReplay(
|
|
186
|
+
item,
|
|
187
|
+
item.type !== "computer_call" && item.id != null ? "drop-id" : "preserve",
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
const CODEX_ORPHAN_OUTPUT_LIMIT = 16_000;
|
package/src/types.ts
CHANGED
|
@@ -240,8 +240,10 @@ export function resolveModelServiceTier(
|
|
|
240
240
|
* `service_tier_for_request`); an empty or missing list counts as "not
|
|
241
241
|
* reported" — accounts whose `/models` lists no tiers keep `/fast` — so the
|
|
242
242
|
* provider-level answer stands. `flex` and `default` are never gated.
|
|
243
|
-
* First-party OpenAI takes `ultrafast` as-is.
|
|
244
|
-
*
|
|
243
|
+
* First-party OpenAI takes `ultrafast` as-is. An OpenAI-family relay on the
|
|
244
|
+
* Responses/Completions APIs gets `ultrafast` only when its discovered
|
|
245
|
+
* `service_tiers` lists it. A bare provider string cannot carry the list, so
|
|
246
|
+
* it answers for the provider alone.
|
|
245
247
|
*/
|
|
246
248
|
export function shouldSendServiceTier(
|
|
247
249
|
serviceTier: ServiceTier | null | undefined,
|
|
@@ -260,7 +262,11 @@ export function shouldSendServiceTier(
|
|
|
260
262
|
if (advertised !== undefined && advertised.length > 0) return advertised.includes(serviceTier);
|
|
261
263
|
}
|
|
262
264
|
if (serviceTier === "ultrafast") {
|
|
263
|
-
|
|
265
|
+
if (typeof target === "string") return provider === "openai" || provider === "openai-codex";
|
|
266
|
+
if (provider === "openai") return true;
|
|
267
|
+
return (
|
|
268
|
+
target !== undefined && isOpenAIServiceTierModel(target) && target.serviceTiers?.includes(serviceTier) === true
|
|
269
|
+
);
|
|
264
270
|
}
|
|
265
271
|
if (provider === "openai" || provider === "openai-codex") return true;
|
|
266
272
|
if (provider === "openrouter") {
|
package/src/utils.ts
CHANGED
|
@@ -111,6 +111,17 @@ export function stripOpenAIResponsesOutputOnlyStatusesForReplay<TItem extends {
|
|
|
111
111
|
return sanitized ?? items;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/** Remove Codex-rejected backend metadata; preserve clean-item identity by default, or copy while keeping/dropping id. */
|
|
115
|
+
export function stripOpenAIResponsesInternalMetadataForReplay<
|
|
116
|
+
TItem extends { id?: unknown; internal_chat_message_metadata_passthrough?: unknown },
|
|
117
|
+
>(item: TItem, mode: "preserve" | "copy" | "drop-id" = "preserve"): TItem {
|
|
118
|
+
if (mode === "preserve" && !Object.hasOwn(item, "internal_chat_message_metadata_passthrough")) return item;
|
|
119
|
+
const sanitized = { ...item };
|
|
120
|
+
delete sanitized.internal_chat_message_metadata_passthrough;
|
|
121
|
+
if (mode === "drop-id") delete sanitized.id;
|
|
122
|
+
return sanitized;
|
|
123
|
+
}
|
|
124
|
+
|
|
114
125
|
/**
|
|
115
126
|
* Clamp `detail: "original"` only where Responses input_image parts live —
|
|
116
127
|
* top-level items and `message.content[]`. Avoids a deep tree walk/clone of
|
|
@@ -614,8 +625,10 @@ function sanitizeOpenAIResponsesHistoryItemForReplay(
|
|
|
614
625
|
if (item.type === "reasoning") {
|
|
615
626
|
return sanitizeOpenAIResponsesReasoningItemForReplay(item, preserveReasoningItemIds);
|
|
616
627
|
}
|
|
617
|
-
const
|
|
618
|
-
|
|
628
|
+
const sanitizedItem = stripOpenAIResponsesInternalMetadataForReplay(
|
|
629
|
+
item,
|
|
630
|
+
item.type === "computer_call" && typeof item.id === "string" ? "copy" : "drop-id",
|
|
631
|
+
);
|
|
619
632
|
if (item.type === "function_call") sanitizedItem.arguments = replayFunctionCallArguments(item);
|
|
620
633
|
|
|
621
634
|
return clampReplayItemImageDetail(
|