@code-yeongyu/senpi-ai 2026.11.4-2 → 2026.11.6-1
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/dist/api/anthropic-messages.js +2 -6
- package/dist/api/openai-images-params.d.ts +5 -0
- package/dist/api/openai-images-params.js +1 -0
- package/dist/api/openai-responses-prompt-cache.d.ts +2 -0
- package/dist/api/openai-responses-prompt-cache.js +6 -4
- package/dist/api/openai-responses-shared.d.ts +1 -0
- package/dist/api/openai-responses-shared.js +6 -1
- package/dist/api/openai-responses.js +75 -4
- package/dist/api/warm-prompt-cache.js +4 -3
- package/dist/auth/pool/slots.d.ts +13 -2
- package/dist/auth/pool/slots.js +136 -14
- package/dist/auth/types.d.ts +13 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/models.js +13 -3
- package/dist/providers/chatgpt-subscription.d.ts +1 -1
- package/dist/providers/chatgpt-subscription.js +68 -8
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/providers/data/baseten.json +1 -1
- package/dist/providers/data/nvidia.json +1 -1
- package/dist/providers/data/openai.json +1 -1
- package/dist/providers/data/opencode-go.json +1 -1
- package/dist/providers/data/opencode.json +1 -1
- package/dist/providers/data/openrouter.json +1 -1
- package/dist/providers/data/radius.json +1 -1
- package/dist/providers/data/together.json +1 -1
- package/dist/providers/data/venice.json +1 -1
- package/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/dist/types.d.ts +3 -2
- package/dist/utils/prompt-cache-ttl.d.ts +9 -0
- package/dist/utils/prompt-cache-ttl.js +15 -3
- package/dist/utils/provider-error.js +3 -2
- package/package.json +2 -2
|
@@ -14,7 +14,7 @@ import { withGzipRequestBodies } from "../utils/gzip-request-body.js";
|
|
|
14
14
|
import { headersToRecord, providerHeadersToRecord } from "../utils/headers.js";
|
|
15
15
|
import { parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse.js";
|
|
16
16
|
import { getPiUserAgent } from "../utils/pi-user-agent.js";
|
|
17
|
-
import {
|
|
17
|
+
import { allowsAnthropicLongCacheRetention, getAnthropicCompat } from "../utils/prompt-cache-ttl.js";
|
|
18
18
|
import { attachProviderDiagnostic } from "../utils/provider-diagnostic-carrier.js";
|
|
19
19
|
import { anthropicProviderDiagnosticFromError, anthropicProviderDiagnosticFromSseData, awaitProviderTransport, } from "../utils/provider-diagnostic-sources.js";
|
|
20
20
|
import { getProviderEnvValue } from "../utils/provider-env.js";
|
|
@@ -63,11 +63,7 @@ function getCacheControl(model, cacheRetention, env) {
|
|
|
63
63
|
if (retention === "none") {
|
|
64
64
|
return { retention };
|
|
65
65
|
}
|
|
66
|
-
const ttl = retention === "long" &&
|
|
67
|
-
isAnthropicApiBaseUrl(model.baseUrl) &&
|
|
68
|
-
getAnthropicCompat(model).supportsLongCacheRetention
|
|
69
|
-
? "1h"
|
|
70
|
-
: undefined;
|
|
66
|
+
const ttl = retention === "long" && allowsAnthropicLongCacheRetention(model) ? "1h" : undefined;
|
|
71
67
|
return {
|
|
72
68
|
retention,
|
|
73
69
|
cacheControl: { type: "ephemeral", ...(ttl && { ttl }) },
|
|
@@ -6,6 +6,8 @@ export type OpenAIImageSize = "auto" | "1024x1024" | "1536x1024" | "1024x1536" |
|
|
|
6
6
|
export type OpenAIImageBackground = "auto" | "transparent" | "opaque";
|
|
7
7
|
export type OpenAIImageOutputFormat = "png" | "jpeg" | "webp";
|
|
8
8
|
export type OpenAIImageModeration = "auto" | "low";
|
|
9
|
+
/** GPT Image 2.6 planning effort before rendering; the API defaults to `high`. */
|
|
10
|
+
export type OpenAIImageComplexity = "low" | "medium" | "high";
|
|
9
11
|
export interface OpenAIImagesOptions extends ImagesOptions {
|
|
10
12
|
size?: OpenAIImageSize;
|
|
11
13
|
quality?: OpenAIImageQuality;
|
|
@@ -17,12 +19,15 @@ export interface OpenAIImagesOptions extends ImagesOptions {
|
|
|
17
19
|
/** 0-100 compression for jpeg/webp output only. */
|
|
18
20
|
outputCompression?: number;
|
|
19
21
|
moderation?: OpenAIImageModeration;
|
|
22
|
+
/** GPT Image 2.6 only; older models reject the field, so omit it for them. */
|
|
23
|
+
complexity?: OpenAIImageComplexity;
|
|
20
24
|
/** Inpainting mask applied to the first input image; requires at least one image input. */
|
|
21
25
|
mask?: ImageContent;
|
|
22
26
|
}
|
|
23
27
|
export type OpenAIImageParams = Omit<ImageGenerateParamsNonStreaming, "size" | "quality"> & {
|
|
24
28
|
size?: OpenAIImageSize;
|
|
25
29
|
quality?: OpenAIImageQuality;
|
|
30
|
+
complexity?: OpenAIImageComplexity;
|
|
26
31
|
image?: Uploadable[];
|
|
27
32
|
mask?: Uploadable;
|
|
28
33
|
};
|
|
@@ -85,6 +85,7 @@ export function buildParams(model, context, options) {
|
|
|
85
85
|
...(options?.background === undefined ? {} : { background: options.background }),
|
|
86
86
|
...(options?.outputCompression === undefined ? {} : { output_compression: options.outputCompression }),
|
|
87
87
|
...(options?.moderation === undefined ? {} : { moderation: options.moderation }),
|
|
88
|
+
...(options?.complexity === undefined ? {} : { complexity: options.complexity }),
|
|
88
89
|
};
|
|
89
90
|
}
|
|
90
91
|
export function isImageParams(value) {
|
|
@@ -9,6 +9,8 @@ export interface OpenAIPromptCacheOptionsPayload {
|
|
|
9
9
|
prewarm?: boolean;
|
|
10
10
|
comparison_response_id?: string;
|
|
11
11
|
}
|
|
12
|
+
/** SIWC plan usage cannot opt into explicit prompt caching through inherited catalog compat. */
|
|
13
|
+
export declare function supportsOpenAIResponsesExplicitPromptCacheMode(model: Model<"openai-responses">): boolean;
|
|
12
14
|
/** Explicit prompt-cache models (GPT-5.6+) on the first-party api.openai.com endpoint. */
|
|
13
15
|
export declare function isOpenAIResponsesPromptCacheModel(model: Model<any>): boolean;
|
|
14
16
|
/**
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
/** SIWC plan usage cannot opt into explicit prompt caching through inherited catalog compat. */
|
|
2
|
+
export function supportsOpenAIResponsesExplicitPromptCacheMode(model) {
|
|
3
|
+
const compat = model.compat;
|
|
4
|
+
return !compat?.siwc && compat?.supportsExplicitPromptCacheMode === true;
|
|
5
|
+
}
|
|
1
6
|
/** Explicit prompt-cache models (GPT-5.6+) on the first-party api.openai.com endpoint. */
|
|
2
7
|
export function isOpenAIResponsesPromptCacheModel(model) {
|
|
3
|
-
if (model.api !== "openai-responses")
|
|
4
|
-
return false;
|
|
5
|
-
const compat = model.compat;
|
|
6
|
-
if (compat?.supportsExplicitPromptCacheMode !== true)
|
|
8
|
+
if (model.api !== "openai-responses" || !supportsOpenAIResponsesExplicitPromptCacheMode(model))
|
|
7
9
|
return false;
|
|
8
10
|
try {
|
|
9
11
|
return new URL(model.baseUrl || "https://api.openai.com/v1").hostname === "api.openai.com";
|
|
@@ -41,6 +41,7 @@ export type ResponsesDeferredToolsMode = "additional-tools" | "tool-search";
|
|
|
41
41
|
export declare function resolveResponsesDeferredToolsMode(compat: {
|
|
42
42
|
supportsAdditionalTools?: boolean;
|
|
43
43
|
supportsToolSearch?: boolean;
|
|
44
|
+
siwc?: boolean;
|
|
44
45
|
} | undefined): ResponsesDeferredToolsMode | undefined;
|
|
45
46
|
export interface ResponsesToolPlacement {
|
|
46
47
|
/** Tools sent in the top-level `tools` field. */
|
|
@@ -122,6 +122,8 @@ function withContextProvenance(item, message, seal) {
|
|
|
122
122
|
export function resolveResponsesDeferredToolsMode(compat) {
|
|
123
123
|
if (compat?.supportsAdditionalTools)
|
|
124
124
|
return "additional-tools";
|
|
125
|
+
if (compat?.siwc)
|
|
126
|
+
return undefined;
|
|
125
127
|
return compat?.supportsToolSearch ? "tool-search" : undefined;
|
|
126
128
|
}
|
|
127
129
|
/**
|
|
@@ -217,7 +219,10 @@ export function convertResponsesMessages(model, context, allowedToolCallProvider
|
|
|
217
219
|
};
|
|
218
220
|
const includeInitialSystemMessage = options?.includeSystemPrompt ?? true;
|
|
219
221
|
const compat = model.compat;
|
|
220
|
-
const instructionRole = model.
|
|
222
|
+
const instructionRole = model.compat?.siwc ||
|
|
223
|
+
(model.reasoning && compat?.supportsDeveloperRole !== false)
|
|
224
|
+
? "developer"
|
|
225
|
+
: "system";
|
|
221
226
|
let msgIndex = 0;
|
|
222
227
|
let sourceIndex = 0;
|
|
223
228
|
for (const msg of transformedMessages) {
|
|
@@ -19,7 +19,7 @@ import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copi
|
|
|
19
19
|
import { resolveOpenAIClientAuth } from "./openai-client-auth.js";
|
|
20
20
|
import { deriveOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
|
|
21
21
|
import { applyAllowedToolsChoice, hasRefusedAllowedToolsChoice, isAllowedToolsChoiceRefusal, rememberAllowedToolsChoiceRefusal, restrictToAllowedTools, } from "./openai-responses-allowed-tools.js";
|
|
22
|
-
import { findPromptCacheComparisonResponseId, withPromptCacheComparison, } from "./openai-responses-prompt-cache.js";
|
|
22
|
+
import { findPromptCacheComparisonResponseId, supportsOpenAIResponsesExplicitPromptCacheMode, withPromptCacheComparison, } from "./openai-responses-prompt-cache.js";
|
|
23
23
|
import { convertResponsesMessages, convertResponsesTools, processResponsesStream, resolveResponsesDeferredToolsMode, resolveResponsesToolPlacement, } from "./openai-responses-shared.js";
|
|
24
24
|
import { buildBaseOptions, clampMaxForOpenAI, OPENAI_RESPONSES_RESERVED_BODY_KEYS } from "./simple-options.js";
|
|
25
25
|
import { startWebSocketLiveness } from "./websocket-liveness.js";
|
|
@@ -69,8 +69,8 @@ function getCompat(model, env) {
|
|
|
69
69
|
supportsStrictMode: model.compat?.supportsStrictMode ?? false,
|
|
70
70
|
supportsOpenAIGrammarTools: model.compat?.supportsOpenAIGrammarTools ?? false,
|
|
71
71
|
supportsAdditionalTools: model.compat?.supportsAdditionalTools ?? false,
|
|
72
|
-
supportsToolSearch: model.compat?.supportsToolSearch ?? false,
|
|
73
|
-
supportsExplicitPromptCacheMode: model
|
|
72
|
+
supportsToolSearch: !model.compat?.siwc && (model.compat?.supportsToolSearch ?? false),
|
|
73
|
+
supportsExplicitPromptCacheMode: supportsOpenAIResponsesExplicitPromptCacheMode(model),
|
|
74
74
|
supportsConfigurationUpdate: model.compat?.supportsConfigurationUpdate ?? false,
|
|
75
75
|
supportsMaxOutputTokens: model.compat?.supportsMaxOutputTokens ?? true,
|
|
76
76
|
supportsForcedToolChoice: model.compat?.supportsForcedToolChoice ?? true,
|
|
@@ -160,6 +160,73 @@ function formatOpenAIResponsesError(error, provider, siwc = false) {
|
|
|
160
160
|
? `${errorMessage}\nCheck your ChatGPT usage: ${CHATGPT_USAGE_URL}`
|
|
161
161
|
: errorMessage;
|
|
162
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* SIWC preview restrictions apply after hooks and sampling overrides too.
|
|
165
|
+
* https://developers.openai.com/siwc/token-sharing-open-source/preview-limitations
|
|
166
|
+
*/
|
|
167
|
+
function shapeSiwcRequest(params) {
|
|
168
|
+
const body = { ...params, store: false, stream: true };
|
|
169
|
+
for (const key of [
|
|
170
|
+
"background",
|
|
171
|
+
"conversation",
|
|
172
|
+
"max_output_tokens",
|
|
173
|
+
"max_tool_calls",
|
|
174
|
+
"metadata",
|
|
175
|
+
"moderation",
|
|
176
|
+
"multi_agent",
|
|
177
|
+
"prompt",
|
|
178
|
+
"prompt_cache_retention",
|
|
179
|
+
"safety_identifier",
|
|
180
|
+
"temperature",
|
|
181
|
+
"top_logprobs",
|
|
182
|
+
"top_p",
|
|
183
|
+
"truncation",
|
|
184
|
+
"user",
|
|
185
|
+
"previous_response_id",
|
|
186
|
+
"prompt_cache_options",
|
|
187
|
+
])
|
|
188
|
+
delete body[key];
|
|
189
|
+
body.input =
|
|
190
|
+
typeof body.input === "string"
|
|
191
|
+
? [{ role: "user", content: body.input }]
|
|
192
|
+
: body.input?.map((item) => "role" in item && item.role === "system" ? { ...item, role: "developer" } : item);
|
|
193
|
+
const tools = body.tools
|
|
194
|
+
?.filter((tool) => tool.type === "function" ||
|
|
195
|
+
tool.type === "custom" ||
|
|
196
|
+
tool.type === "namespace" ||
|
|
197
|
+
tool.type === "web_search")
|
|
198
|
+
.map((tool) => tool.type === "namespace"
|
|
199
|
+
? {
|
|
200
|
+
...tool,
|
|
201
|
+
tools: tool.tools.filter((nested) => nested.type === "function" || nested.type === "custom"),
|
|
202
|
+
}
|
|
203
|
+
: tool);
|
|
204
|
+
// SIWC accepts local tools in namespaces or additional_tools, not flat top-level definitions.
|
|
205
|
+
const eagerTools = tools?.filter((tool) => tool.type === "function" || tool.type === "custom") ?? [];
|
|
206
|
+
body.tools = tools?.filter((tool) => tool.type !== "function" && tool.type !== "custom");
|
|
207
|
+
if (eagerTools.length > 0) {
|
|
208
|
+
Reflect.set(body, "input", [
|
|
209
|
+
{ type: "additional_tools", role: "developer", tools: eagerTools },
|
|
210
|
+
...(body.input ?? []),
|
|
211
|
+
]);
|
|
212
|
+
}
|
|
213
|
+
const choice = body.tool_choice;
|
|
214
|
+
if (isRecord(choice)) {
|
|
215
|
+
if (choice.type === "allowed_tools" && Array.isArray(choice.tools)) {
|
|
216
|
+
const tools = choice.tools.filter((tool) => isRecord(tool) && (tool.type === "function" || tool.type === "custom" || tool.type === "web_search"));
|
|
217
|
+
Reflect.set(body, "tool_choice", tools.length > 0
|
|
218
|
+
? {
|
|
219
|
+
...choice,
|
|
220
|
+
tools,
|
|
221
|
+
}
|
|
222
|
+
: "none");
|
|
223
|
+
}
|
|
224
|
+
else if (choice.type !== "function" && choice.type !== "custom" && choice.type !== "web_search") {
|
|
225
|
+
delete body.tool_choice;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return body;
|
|
229
|
+
}
|
|
163
230
|
/**
|
|
164
231
|
* Generate function for OpenAI Responses API
|
|
165
232
|
*/
|
|
@@ -206,7 +273,9 @@ export const stream = (model, context, options) => {
|
|
|
206
273
|
params = { ...params, tools: limitedTools.tools };
|
|
207
274
|
recordGitHubCopilotToolLimit(output, limitedTools.omittedCount);
|
|
208
275
|
}
|
|
209
|
-
|
|
276
|
+
if (compat.siwc)
|
|
277
|
+
params = shapeSiwcRequest(params);
|
|
278
|
+
const transport = compat.siwc ? "sse" : (options?.transport ?? "sse");
|
|
210
279
|
let refusedAllowedToolsOverWebSocket = false;
|
|
211
280
|
if (transport !== "sse" && compat.supportsWebSocket) {
|
|
212
281
|
let websocketStarted = false;
|
|
@@ -346,6 +415,8 @@ function resolveSimpleOptions(model, context, options) {
|
|
|
346
415
|
* prefix matches the reasoning effort, service tier, and cache options that turn sends.
|
|
347
416
|
*/
|
|
348
417
|
export async function warmOpenAIResponsesPromptCache(model, context, options) {
|
|
418
|
+
if (model.compat?.siwc)
|
|
419
|
+
throw new Error("SIWC requires streaming inference; prompt-cache prewarm is unsupported");
|
|
349
420
|
const prefix = normalizeContext({ ...context, messages: [] });
|
|
350
421
|
const resolved = resolveSimpleOptions(model, prefix, options);
|
|
351
422
|
const clientAuth = resolveOpenAIClientAuth(model.provider, resolved.apiKey, resolved.headers);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { withGzipRequestBodies } from "../utils/gzip-request-body.js";
|
|
2
2
|
import { providerHeadersToRecord } from "../utils/headers.js";
|
|
3
|
-
import { getAnthropicCompat,
|
|
3
|
+
import { getAnthropicCompat, supportsAnthropicCacheWarming } from "../utils/prompt-cache-ttl.js";
|
|
4
4
|
import { isOpenAIResponsesPromptCacheModel } from "./openai-responses-prompt-cache.js";
|
|
5
5
|
import { getPromptCacheWarmer } from "./prompt-cache-warmers.js";
|
|
6
6
|
export async function warmPromptCache(model, context, options = {}) {
|
|
@@ -24,10 +24,11 @@ export async function warmPromptCache(model, context, options = {}) {
|
|
|
24
24
|
usageRaw,
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
-
if (model.api !== "anthropic-messages"
|
|
27
|
+
if (model.api !== "anthropic-messages")
|
|
28
28
|
return { supported: false };
|
|
29
|
-
}
|
|
30
29
|
const anthropicModel = model;
|
|
30
|
+
if (!supportsAnthropicCacheWarming(anthropicModel))
|
|
31
|
+
return { supported: false };
|
|
31
32
|
// Loaded lazily: a static import would pull the Anthropic SDK into the root
|
|
32
33
|
// barrel and break the lazy provider-loading contract (lazy-module-load.test.ts).
|
|
33
34
|
const [{ default: Anthropic }, { buildAnthropicWarmPromptCacheParams }] = await Promise.all([
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { ProviderEnv } from "../../types.ts";
|
|
2
|
-
import type { Credential } from "../types.ts";
|
|
2
|
+
import type { Credential, OAuthCredentials } from "../types.ts";
|
|
3
3
|
export type { Credential };
|
|
4
4
|
export declare const DEFAULT_SLOT_NAME = "default";
|
|
5
5
|
export type CredentialSlotSource = "login" | "import" | "env";
|
|
6
|
-
export type CredentialSlot = {
|
|
6
|
+
export type CredentialSlot = Partial<OAuthCredentials> & {
|
|
7
7
|
/** Immutable operational identity. */
|
|
8
8
|
name: string;
|
|
9
9
|
/** Optional presentation metadata; never used for credential selection. */
|
|
@@ -77,6 +77,15 @@ export declare class EmptyApiKeyError extends Error {
|
|
|
77
77
|
readonly name = "EmptyApiKeyError";
|
|
78
78
|
constructor();
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* A reauthorization signed in a different ChatGPT account than the saved one it was started for (#5003). Nothing
|
|
82
|
+
* is written: the selected account keeps its registration and the other sign-in is not silently swapped in.
|
|
83
|
+
*/
|
|
84
|
+
export declare class ReauthorizeAccountMismatchError extends Error {
|
|
85
|
+
readonly name = "ReauthorizeAccountMismatchError";
|
|
86
|
+
readonly slotName: string;
|
|
87
|
+
constructor(slotName: string);
|
|
88
|
+
}
|
|
80
89
|
/** Drops a blank or missing `key` from an API-key credential; used only for providers that declare `keyOptional`. */
|
|
81
90
|
export declare function withoutBlankKey(credential: Credential): Credential;
|
|
82
91
|
/**
|
|
@@ -111,6 +120,8 @@ export type AppendLoginSlotOptions = {
|
|
|
111
120
|
accountIdentity?: (credential: {
|
|
112
121
|
access: string;
|
|
113
122
|
}) => string | undefined;
|
|
123
|
+
/** `LoginOptions.reauthorizeSlot`: the saved account this login reauthorizes. */
|
|
124
|
+
reauthorizeSlot?: string;
|
|
114
125
|
};
|
|
115
126
|
/**
|
|
116
127
|
* Material a provider writes into the flat credential when the real token lives
|
package/dist/auth/pool/slots.js
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import { LEGACY_PROVIDER_IDS } from "../../legacy-provider-ids.js";
|
|
2
2
|
export const DEFAULT_SLOT_NAME = "default";
|
|
3
|
+
const OAUTH_ACCOUNT_FIELDS = [
|
|
4
|
+
"route",
|
|
5
|
+
"issuer",
|
|
6
|
+
"subject",
|
|
7
|
+
"clientId",
|
|
8
|
+
"email",
|
|
9
|
+
"idToken",
|
|
10
|
+
"scopes",
|
|
11
|
+
"planUsageEnabled",
|
|
12
|
+
"signedOut",
|
|
13
|
+
];
|
|
14
|
+
/** Replace account metadata as a unit; an absent field must not inherit a sibling's identity. */
|
|
15
|
+
function withOAuthAccountFields(target, source) {
|
|
16
|
+
const next = { ...target };
|
|
17
|
+
for (const field of OAUTH_ACCOUNT_FIELDS) {
|
|
18
|
+
if (source[field] === undefined)
|
|
19
|
+
Reflect.deleteProperty(next, field);
|
|
20
|
+
else
|
|
21
|
+
Reflect.set(next, field, source[field]);
|
|
22
|
+
}
|
|
23
|
+
return next;
|
|
24
|
+
}
|
|
3
25
|
const SLOT_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
4
26
|
export function assertValidSlotName(name) {
|
|
5
27
|
if (!SLOT_NAME_PATTERN.test(name)) {
|
|
@@ -178,7 +200,7 @@ function projectFlatFields(credential, slot) {
|
|
|
178
200
|
if (credential.type === "oauth") {
|
|
179
201
|
if (slot.access === undefined || slot.refresh === undefined || slot.expires === undefined)
|
|
180
202
|
return credential;
|
|
181
|
-
return { ...credential, access: slot.access, refresh: slot.refresh, expires: slot.expires, ...env };
|
|
203
|
+
return withOAuthAccountFields({ ...credential, access: slot.access, refresh: slot.refresh, expires: slot.expires, ...env }, slot);
|
|
182
204
|
}
|
|
183
205
|
return { ...credential, key: slot.key, ...env };
|
|
184
206
|
}
|
|
@@ -228,20 +250,20 @@ export function projectSlot(credential, name) {
|
|
|
228
250
|
if (flat.type === "oauth") {
|
|
229
251
|
if (slot.access === undefined || slot.refresh === undefined || slot.expires === undefined)
|
|
230
252
|
return undefined;
|
|
231
|
-
return { ...flat, access: slot.access, refresh: slot.refresh, expires: slot.expires, ...env };
|
|
253
|
+
return withOAuthAccountFields({ ...flat, access: slot.access, refresh: slot.refresh, expires: slot.expires, ...env }, slot);
|
|
232
254
|
}
|
|
233
255
|
return { ...flat, key: slot.key, ...env };
|
|
234
256
|
}
|
|
235
257
|
function slotFromFlatCredentialNamed(credential, name) {
|
|
236
258
|
if (credential.type === "oauth") {
|
|
237
|
-
return {
|
|
259
|
+
return withOAuthAccountFields({
|
|
238
260
|
name,
|
|
239
261
|
source: "login",
|
|
240
262
|
access: credential.access,
|
|
241
263
|
refresh: credential.refresh,
|
|
242
264
|
expires: credential.expires,
|
|
243
265
|
...credentialSlotEnv(credential),
|
|
244
|
-
};
|
|
266
|
+
}, credential);
|
|
245
267
|
}
|
|
246
268
|
return { name, source: "login", key: credential.key, ...credentialSlotEnv(credential) };
|
|
247
269
|
}
|
|
@@ -296,6 +318,17 @@ export class EmptyApiKeyError extends Error {
|
|
|
296
318
|
this.name = "EmptyApiKeyError";
|
|
297
319
|
}
|
|
298
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* A reauthorization signed in a different ChatGPT account than the saved one it was started for (#5003). Nothing
|
|
323
|
+
* is written: the selected account keeps its registration and the other sign-in is not silently swapped in.
|
|
324
|
+
*/
|
|
325
|
+
export class ReauthorizeAccountMismatchError extends Error {
|
|
326
|
+
constructor(slotName) {
|
|
327
|
+
super(`Signed in to a different ChatGPT account than '${slotName}'; nothing was changed. Sign in as that account, or add the other one as a new account.`);
|
|
328
|
+
this.name = "ReauthorizeAccountMismatchError";
|
|
329
|
+
this.slotName = slotName;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
299
332
|
/** True when an API-key credential names a `key` that is blank, whitespace or not a string. */
|
|
300
333
|
function hasBlankKey(credential) {
|
|
301
334
|
if (credential.type !== "api_key" || !Object.hasOwn(credential, "key"))
|
|
@@ -358,6 +391,11 @@ export function appendLoginSlot(stored, flat, onAllocated, options) {
|
|
|
358
391
|
onAllocated?.(DEFAULT_SLOT_NAME, "generated");
|
|
359
392
|
return flat;
|
|
360
393
|
}
|
|
394
|
+
const target = provided || options?.reauthorizeSlot === undefined ? undefined : findSlot(current, options.reauthorizeSlot);
|
|
395
|
+
if (target) {
|
|
396
|
+
assertSameReauthorizedAccount(target, flat, options?.accountIdentity);
|
|
397
|
+
return restoreReauthorizedSlot(current, target, flat, onAllocated);
|
|
398
|
+
}
|
|
361
399
|
const storedAccounts = Array.isArray(current.accounts) ? current.accounts : undefined;
|
|
362
400
|
if (!provided && "signedOut" in current && current.signedOut === true) {
|
|
363
401
|
// Sign-out keeps only the registration for a later sign-in (senpi#5003 W7). That sign-in takes over
|
|
@@ -374,7 +412,31 @@ export function appendLoginSlot(stored, flat, onAllocated, options) {
|
|
|
374
412
|
// shape because the provider's accounts already carry this login.
|
|
375
413
|
return storedAccounts ? mergeProvidedPool(current, storedAccounts, provided) : flat;
|
|
376
414
|
}
|
|
377
|
-
|
|
415
|
+
if (flat.type === "oauth" && flat.route === "siwc") {
|
|
416
|
+
const existing = listSlots(current);
|
|
417
|
+
const matched = existing.find((slot) => slot.route === "siwc" &&
|
|
418
|
+
["issuer", "subject", "clientId"].every((field) => typeof flat[field] === "string" && flat[field].trim() !== "" && flat[field] === slot[field]));
|
|
419
|
+
if (matched) {
|
|
420
|
+
const replacement = {
|
|
421
|
+
...slotFromFlatCredentialNamed(flat, matched.name),
|
|
422
|
+
...(matched.displayName === undefined ? {} : { displayName: matched.displayName }),
|
|
423
|
+
};
|
|
424
|
+
const next = slotMirrorsFlat(current, matched) ? projectFlatFields(current, replacement) : current;
|
|
425
|
+
onAllocated?.(matched.name, "generated");
|
|
426
|
+
if (!storedAccounts)
|
|
427
|
+
return next;
|
|
428
|
+
const pooled = {
|
|
429
|
+
...next,
|
|
430
|
+
accounts: existing.map((slot) => (slot === matched ? replacement : slot)),
|
|
431
|
+
};
|
|
432
|
+
return pooled;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// Sign in with ChatGPT names an account by its issuer, subject and issued client ID (matched above). Without
|
|
436
|
+
// that tuple, a shared ChatGPT token identity is not the same SIWC sign-in, so the login is appended.
|
|
437
|
+
const replaced = flat.type === "oauth" && flat.route === "siwc"
|
|
438
|
+
? undefined
|
|
439
|
+
: replaceSameAccount(current, flat, options?.accountIdentity, onAllocated);
|
|
378
440
|
if (replaced)
|
|
379
441
|
return replaced;
|
|
380
442
|
const name = nextLoginSlotName(current);
|
|
@@ -382,6 +444,62 @@ export function appendLoginSlot(stored, flat, onAllocated, options) {
|
|
|
382
444
|
onAllocated?.(name, "generated");
|
|
383
445
|
return next;
|
|
384
446
|
}
|
|
447
|
+
/**
|
|
448
|
+
* A reauthorization of a named saved account rewrites that account and nothing else (#5003). A signed-out account
|
|
449
|
+
* holds blank tokens, so neither the token-identity match nor the sign-in takeover can find it, and both used to
|
|
450
|
+
* append a duplicate `login-N`. The account keeps its name, label and source and drops `signedOut`; the flat
|
|
451
|
+
* projection moves to it when it carried that account or when the whole credential was signed out.
|
|
452
|
+
*/
|
|
453
|
+
/**
|
|
454
|
+
* The identity a reauthorization must keep. SIWC names an account by issuer, subject and issued client ID, which a
|
|
455
|
+
* sign-out keeps. A legacy Codex account is named by its token identity, or, once signed out and without a token,
|
|
456
|
+
* by its workspace (`accountId`). A field the saved account never recorded cannot be compared and is not checked.
|
|
457
|
+
*/
|
|
458
|
+
function assertSameReauthorizedAccount(target, flat, accountIdentity) {
|
|
459
|
+
if (flat.type !== "oauth")
|
|
460
|
+
return;
|
|
461
|
+
const saved = target;
|
|
462
|
+
const fresh = flat;
|
|
463
|
+
const differs = (field) => typeof saved[field] === "string" &&
|
|
464
|
+
saved[field] !== "" &&
|
|
465
|
+
typeof fresh[field] === "string" &&
|
|
466
|
+
saved[field] !== fresh[field];
|
|
467
|
+
if ((saved.route ?? "codex") !== (flat.route ?? "codex"))
|
|
468
|
+
throw new ReauthorizeAccountMismatchError(target.name);
|
|
469
|
+
if (flat.route === "siwc") {
|
|
470
|
+
if (["issuer", "subject", "clientId"].some(differs))
|
|
471
|
+
throw new ReauthorizeAccountMismatchError(target.name);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const savedIdentity = accountIdentity && typeof target.access === "string" && target.access !== ""
|
|
475
|
+
? accountIdentity({ access: target.access })
|
|
476
|
+
: undefined;
|
|
477
|
+
const freshIdentity = accountIdentity?.(flat);
|
|
478
|
+
if (savedIdentity !== undefined && freshIdentity !== undefined) {
|
|
479
|
+
if (savedIdentity !== freshIdentity)
|
|
480
|
+
throw new ReauthorizeAccountMismatchError(target.name);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (differs("accountId"))
|
|
484
|
+
throw new ReauthorizeAccountMismatchError(target.name);
|
|
485
|
+
}
|
|
486
|
+
function restoreReauthorizedSlot(current, target, flat, onAllocated) {
|
|
487
|
+
onAllocated?.(target.name, "generated");
|
|
488
|
+
if (!Array.isArray(current.accounts) || current.accounts.length === 0)
|
|
489
|
+
return flat;
|
|
490
|
+
const fresh = {
|
|
491
|
+
...slotFromFlatCredentialNamed(flat, target.name),
|
|
492
|
+
...(target.displayName === undefined ? {} : { displayName: target.displayName }),
|
|
493
|
+
...(target.source === undefined ? {} : { source: target.source }),
|
|
494
|
+
};
|
|
495
|
+
const accounts = listSlots(current).map((slot) => (slot.name === target.name ? fresh : slot));
|
|
496
|
+
const wholeSignedOut = "signedOut" in current && current.signedOut === true;
|
|
497
|
+
const reproject = wholeSignedOut || slotMirrorsFlat(current, target);
|
|
498
|
+
const next = { ...(reproject ? projectFlatFields(current, fresh) : current), accounts };
|
|
499
|
+
if (reproject)
|
|
500
|
+
Reflect.deleteProperty(next, "signedOut");
|
|
501
|
+
return next;
|
|
502
|
+
}
|
|
385
503
|
/**
|
|
386
504
|
* A login of an account that is already saved rewrites that account instead of
|
|
387
505
|
* appending another `login-N` copy of it. Appending kept the old copy, and once the
|
|
@@ -405,7 +523,13 @@ function replaceSameAccount(current, flat, accountIdentity, onAllocated) {
|
|
|
405
523
|
if (identity === undefined)
|
|
406
524
|
return undefined;
|
|
407
525
|
const slots = listSlots(current);
|
|
408
|
-
|
|
526
|
+
// A token identity names a ChatGPT account, not a sign-in: the legacy Codex route and Sign in with ChatGPT
|
|
527
|
+
// hold separate grants for one person, so a login replaces only a slot on its own route (#5003).
|
|
528
|
+
const route = flat.route ?? "codex";
|
|
529
|
+
const matches = slots.filter((slot) => slot.source !== "env" &&
|
|
530
|
+
(slot.route ?? "codex") === route &&
|
|
531
|
+
slot.access !== undefined &&
|
|
532
|
+
accountIdentity({ access: slot.access }) === identity);
|
|
409
533
|
const kept = matches[0];
|
|
410
534
|
if (!kept)
|
|
411
535
|
return undefined;
|
|
@@ -413,14 +537,12 @@ function replaceSameAccount(current, flat, accountIdentity, onAllocated) {
|
|
|
413
537
|
onAllocated?.(kept.name, "generated");
|
|
414
538
|
return flat;
|
|
415
539
|
}
|
|
540
|
+
// Built through the account-field projection so the login's route, issued client, issuer, subject, ID token
|
|
541
|
+
// and scopes reach the slot; a hand-copied token triple dropped them (#5003).
|
|
416
542
|
const fresh = {
|
|
417
|
-
|
|
543
|
+
...slotFromFlatCredentialNamed(flat, kept.name),
|
|
418
544
|
...(kept.displayName === undefined ? {} : { displayName: kept.displayName }),
|
|
419
545
|
...(kept.source === undefined ? {} : { source: kept.source }),
|
|
420
|
-
access: flat.access,
|
|
421
|
-
refresh: flat.refresh,
|
|
422
|
-
expires: flat.expires,
|
|
423
|
-
...credentialSlotEnv(flat),
|
|
424
546
|
};
|
|
425
547
|
const dropped = new Set(matches.slice(1).map((slot) => slot.name));
|
|
426
548
|
const accounts = slots.flatMap((slot) => (slot === kept ? [fresh] : dropped.has(slot.name) ? [] : [slot]));
|
|
@@ -510,7 +632,7 @@ export function mergeRefreshedSlot(current, name, refreshed) {
|
|
|
510
632
|
const target = current.accounts.find((slot) => slot.name === name);
|
|
511
633
|
if (!target)
|
|
512
634
|
return current;
|
|
513
|
-
const rotated = { access: refreshed.access, refresh: refreshed.refresh, expires: refreshed.expires };
|
|
635
|
+
const rotated = withOAuthAccountFields({ access: refreshed.access, refresh: refreshed.refresh, expires: refreshed.expires }, refreshed);
|
|
514
636
|
const accounts = current.accounts.map((slot) => (slot === target ? { ...slot, ...rotated } : slot));
|
|
515
637
|
const mirrorsFlat = target.access === current.access || target.refresh === current.refresh;
|
|
516
638
|
return mirrorsFlat ? { ...current, ...rotated, accounts } : { ...current, accounts };
|
|
@@ -522,11 +644,11 @@ export function mergeRefreshed(current, refreshed) {
|
|
|
522
644
|
if (refreshed.type !== "oauth" || current.type !== "oauth")
|
|
523
645
|
return refreshed;
|
|
524
646
|
const target = current.accounts.find((slot) => slot.access === current.access || slot.refresh === current.refresh);
|
|
525
|
-
const rotated = {
|
|
647
|
+
const rotated = withOAuthAccountFields({
|
|
526
648
|
access: refreshed.access,
|
|
527
649
|
refresh: refreshed.refresh,
|
|
528
650
|
expires: refreshed.expires,
|
|
529
|
-
};
|
|
651
|
+
}, refreshed);
|
|
530
652
|
const accounts = target
|
|
531
653
|
? current.accounts.map((slot) => (slot === target ? { ...slot, ...rotated } : slot))
|
|
532
654
|
: current.accounts;
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import type { ProviderEnv, ProviderHeaders } from "../types.ts";
|
|
1
|
+
import type { Api, Model, ProviderEnv, ProviderHeaders } from "../types.ts";
|
|
2
2
|
/**
|
|
3
3
|
* Request auth for a single model request. If a value cannot be expressed as
|
|
4
|
-
* `apiKey`, `headers`,
|
|
4
|
+
* `apiKey`, `headers`, `baseUrl`, or the credential's chat route, it is provider config, not auth.
|
|
5
5
|
*/
|
|
6
6
|
export interface ModelAuth {
|
|
7
7
|
apiKey?: string;
|
|
8
8
|
headers?: ProviderHeaders;
|
|
9
9
|
baseUrl?: string;
|
|
10
|
+
/** Credential-selected chat transport; absent preserves the selected model's API. */
|
|
11
|
+
api?: Api;
|
|
12
|
+
/** Request restrictions belonging to the selected credential's chat route. */
|
|
13
|
+
compat?: Model<Api>["compat"];
|
|
10
14
|
}
|
|
11
15
|
/**
|
|
12
16
|
* Stored api-key credential. `env` holds provider-scoped environment/config
|
|
@@ -24,6 +28,8 @@ export interface OAuthCredentials {
|
|
|
24
28
|
refresh: string;
|
|
25
29
|
access: string;
|
|
26
30
|
expires: number;
|
|
31
|
+
/** ChatGPT slots without a route retain the legacy Codex protocol. */
|
|
32
|
+
route?: "siwc" | "codex";
|
|
27
33
|
[key: string]: unknown;
|
|
28
34
|
}
|
|
29
35
|
/** Stored canonical OAuth credential. */
|
|
@@ -268,6 +274,11 @@ export interface LoginOptions {
|
|
|
268
274
|
* an account; callers must not infer this from the provider's active credential.
|
|
269
275
|
*/
|
|
270
276
|
reauthorize?: OAuthCredential;
|
|
277
|
+
/**
|
|
278
|
+
* The saved account name of `reauthorize`. The login then restores that account in place, signed-out ones
|
|
279
|
+
* included, instead of matching by token or appending another `login-N` copy of it.
|
|
280
|
+
*/
|
|
281
|
+
reauthorizeSlot?: string;
|
|
271
282
|
/**
|
|
272
283
|
* Returns the stable ID of this app installation, e.g. sent to OpenAI as its
|
|
273
284
|
* agent host ID. Called only by login flows that need it, so apps can create
|
package/dist/index.d.ts
CHANGED
|
@@ -58,7 +58,7 @@ export * from "./utils/event-stream.ts";
|
|
|
58
58
|
export * from "./utils/json-parse.ts";
|
|
59
59
|
export * from "./utils/overflow.ts";
|
|
60
60
|
export * from "./utils/provider-error.ts";
|
|
61
|
-
export { isAnthropicApiBaseUrl, PROMPT_CACHE_TTL_LONG_SECONDS, PROMPT_CACHE_TTL_OPENAI_EXTENDED_SECONDS, PROMPT_CACHE_TTL_SHORT_SECONDS, type PromptCacheLifetime, resolvePromptCacheLifetime, resolvePromptCacheTtlSeconds, } from "./utils/prompt-cache-ttl.ts";
|
|
61
|
+
export { isAnthropicApiBaseUrl, PROMPT_CACHE_TTL_LONG_SECONDS, PROMPT_CACHE_TTL_OPENAI_EXTENDED_SECONDS, PROMPT_CACHE_TTL_SHORT_SECONDS, type PromptCacheLifetime, resolvePromptCacheLifetime, resolvePromptCacheTtlSeconds, supportsAnthropicCacheWarming, } from "./utils/prompt-cache-ttl.ts";
|
|
62
62
|
export * from "./utils/provider-failure-description.ts";
|
|
63
63
|
export * from "./utils/retry.ts";
|
|
64
64
|
export * from "./utils/server-fallback-receipt.ts";
|
package/dist/index.js
CHANGED
|
@@ -44,7 +44,7 @@ export * from "./utils/event-stream.js";
|
|
|
44
44
|
export * from "./utils/json-parse.js";
|
|
45
45
|
export * from "./utils/overflow.js";
|
|
46
46
|
export * from "./utils/provider-error.js";
|
|
47
|
-
export { isAnthropicApiBaseUrl, PROMPT_CACHE_TTL_LONG_SECONDS, PROMPT_CACHE_TTL_OPENAI_EXTENDED_SECONDS, PROMPT_CACHE_TTL_SHORT_SECONDS, resolvePromptCacheLifetime, resolvePromptCacheTtlSeconds, } from "./utils/prompt-cache-ttl.js";
|
|
47
|
+
export { isAnthropicApiBaseUrl, PROMPT_CACHE_TTL_LONG_SECONDS, PROMPT_CACHE_TTL_OPENAI_EXTENDED_SECONDS, PROMPT_CACHE_TTL_SHORT_SECONDS, resolvePromptCacheLifetime, resolvePromptCacheTtlSeconds, supportsAnthropicCacheWarming, } from "./utils/prompt-cache-ttl.js";
|
|
48
48
|
export * from "./utils/provider-failure-description.js";
|
|
49
49
|
export * from "./utils/retry.js";
|
|
50
50
|
export * from "./utils/server-fallback-receipt.js";
|
package/dist/models.js
CHANGED
|
@@ -2,7 +2,7 @@ import { lazyStream } from "./api/lazy.js";
|
|
|
2
2
|
import { defaultProviderAuthContext as defaultAuthContext } from "./auth/context.js";
|
|
3
3
|
import { InMemoryCredentialStore, revokeStoredOAuth } from "./auth/credential-store.js";
|
|
4
4
|
import { stampLogin } from "./auth/pool/login-id.js";
|
|
5
|
-
import { appendLoginSlot, assertLoginKeyPresent, removeSlot, withoutBlankKey } from "./auth/pool/slots.js";
|
|
5
|
+
import { appendLoginSlot, assertLoginKeyPresent, ReauthorizeAccountMismatchError, removeSlot, withoutBlankKey, } from "./auth/pool/slots.js";
|
|
6
6
|
import { resolveRefreshCredential } from "./auth/refresh-credential.js";
|
|
7
7
|
import { ModelsError, providerNotConfiguredMessage, resolveProviderAuth, } from "./auth/resolve.js";
|
|
8
8
|
import { inferGoogleThinkingLevelMap } from "./google-thinking-generation.js";
|
|
@@ -385,7 +385,7 @@ class ModelsImpl {
|
|
|
385
385
|
return stampLogin(appendLoginSlot(current, credential, (name, origin) => {
|
|
386
386
|
committedName = name;
|
|
387
387
|
committedOrigin = origin;
|
|
388
|
-
}, { accountIdentity: provider.auth.oauth?.accountIdentity }));
|
|
388
|
+
}, { accountIdentity: provider.auth.oauth?.accountIdentity, reauthorizeSlot: options?.reauthorizeSlot }));
|
|
389
389
|
}, { signal });
|
|
390
390
|
void mutation.catch(() => { });
|
|
391
391
|
try {
|
|
@@ -409,6 +409,9 @@ class ModelsImpl {
|
|
|
409
409
|
}
|
|
410
410
|
catch (error) {
|
|
411
411
|
signal.throwIfAborted();
|
|
412
|
+
// The user must see why a reauthorization changed nothing, not a storage failure.
|
|
413
|
+
if (error instanceof ReauthorizeAccountMismatchError)
|
|
414
|
+
throw error;
|
|
412
415
|
throw new ModelsError("auth", `Credential store modify failed for ${providerId}`, { cause: error });
|
|
413
416
|
}
|
|
414
417
|
if (committedName !== undefined && committedOrigin !== undefined) {
|
|
@@ -496,7 +499,14 @@ class ModelsImpl {
|
|
|
496
499
|
if (options?.transformHeaders)
|
|
497
500
|
headers = await options.transformHeaders(headers ?? {});
|
|
498
501
|
const env = resolution.env || options?.env ? { ...(resolution.env ?? {}), ...(options?.env ?? {}) } : undefined;
|
|
499
|
-
const requestModel = auth.baseUrl
|
|
502
|
+
const requestModel = auth.baseUrl || ((auth.api || auth.compat) && isModelType(model, "chat"))
|
|
503
|
+
? {
|
|
504
|
+
...model,
|
|
505
|
+
...(auth.baseUrl ? { baseUrl: auth.baseUrl } : {}),
|
|
506
|
+
...(auth.api && isModelType(model, "chat") ? { api: auth.api } : {}),
|
|
507
|
+
...(auth.compat && isModelType(model, "chat") ? { compat: { ...model.compat, ...auth.compat } } : {}),
|
|
508
|
+
}
|
|
509
|
+
: model;
|
|
500
510
|
const { transformHeaders: _transformHeaders, ...providerOptions } = options ?? {};
|
|
501
511
|
const requestOptions = { ...providerOptions, apiKey, headers, env };
|
|
502
512
|
return { requestModel, requestOptions };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { type Provider } from "../models.ts";
|
|
2
|
-
export declare function chatgptSubscriptionProvider(): Provider<"openai-codex-responses">;
|
|
2
|
+
export declare function chatgptSubscriptionProvider(): Provider<"openai-codex-responses" | "openai-responses">;
|
|
3
3
|
//# sourceMappingURL=chatgpt-subscription.d.ts.map
|