feat: refine language controls and add per-tier ChatGPT Fast mode
This commit is contained in:
@@ -12,6 +12,7 @@ export function chatGPTStreamError(message: string): string {
|
||||
if (/expired|invalid_grant|401|invalid_user/.test(message)) return "Your ChatGPT session expired. Reconnect in Providers. No API-key fallback was used.";
|
||||
if (/background|Background/.test(message)) return "ChatGPT background use is off in this app. Enable Allow background use for all ChatGPT accounts in Providers to run this work. No API-key fallback was used.";
|
||||
if (/eligible|403/.test(message)) return "This ChatGPT account or workspace does not permit this request. Check ChatGPT settings; reconnecting may not change eligibility.";
|
||||
if (/service[_ -]?tier|priority processing|fast mode/i.test(message)) return "Fast mode is unavailable for this ChatGPT model or account. Turn off Fast mode or choose another model.";
|
||||
if (/cancel|abort|closed|disconnect/i.test(message)) return "ChatGPT request cancelled or connection closed. No API-key fallback was used.";
|
||||
if (/catalog|model/.test(message)) return "The selected model is unavailable for this ChatGPT account. Refresh Providers and explicitly choose an available model. No API-key fallback was used.";
|
||||
if (/incomplete|terminal|interrupted|stop reason/.test(message)) return "ChatGPT returned an incomplete response. Retry when ready; no API-key fallback was used.";
|
||||
@@ -19,9 +20,10 @@ export function chatGPTStreamError(message: string): string {
|
||||
}
|
||||
|
||||
/** Restrict the body at the final payload seam, even if caller samplingParams add fields. */
|
||||
export function chatGPTPayload(raw: unknown, selectedModelId: string): Record<string, unknown> {
|
||||
export function chatGPTPayload(raw: unknown, selectedModelId: string, fastMode = false): Record<string, unknown> {
|
||||
const payload = raw as Record<string, unknown>;
|
||||
const out: Record<string, unknown> = { model: selectedModelId, store: false, stream: true };
|
||||
if (fastMode) out.service_tier = "priority";
|
||||
for (const key of ["instructions", "reasoning", "include", "text", "tool_choice", "parallel_tool_calls", "prompt_cache_key"]) {
|
||||
if (payload[key] !== undefined) out[key] = payload[key];
|
||||
}
|
||||
@@ -44,7 +46,7 @@ export function chatGPTHistory(context: TranscriptContext, logicalModel: Model<A
|
||||
function empty(model: Model<Api>): AssistantMessage {
|
||||
return { role: "assistant", api: model.api, provider: "chatgpt", model: model.id, content: [], stopReason: "error", timestamp: Date.now(), usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } } };
|
||||
}
|
||||
export function chatGPTStream(background: () => boolean, lifecycle?: ChatGPTDispatchLifecycle): StreamFunction<Api, SimpleStreamOptions> {
|
||||
export function chatGPTStream(background: () => boolean, lifecycle?: ChatGPTDispatchLifecycle, fastMode = false): StreamFunction<Api, SimpleStreamOptions> {
|
||||
return (logicalModel, context, options) => {
|
||||
const output = new AssistantMessageEventStream();
|
||||
const purpose = background();
|
||||
@@ -83,14 +85,18 @@ export function chatGPTStream(background: () => boolean, lifecycle?: ChatGPTDisp
|
||||
},
|
||||
onPayload: async (raw, model) => {
|
||||
const replacement = await options?.onPayload?.(raw, model);
|
||||
const payload = chatGPTPayload(replacement === undefined ? raw : replacement, logicalModel.id);
|
||||
const payload = chatGPTPayload(replacement === undefined ? raw : replacement, logicalModel.id, fastMode);
|
||||
check();
|
||||
return payload;
|
||||
},
|
||||
onProviderStreamEvent: async (raw, model) => {
|
||||
const event = raw as { type?: string; code?: string; error?: { code?: string }; response?: { error?: { code?: string }; status?: string } };
|
||||
const event = raw as { type?: string; code?: string; param?: string; error?: { code?: string; param?: string }; response?: { error?: { code?: string; param?: string }; status?: string } };
|
||||
if (event.type === "response.completed" && event.response?.status === "completed" && !event.response?.error) completed = true;
|
||||
if (["response.incomplete", "response.failed", "error"].includes(event.type ?? "") || event.response?.error) failure = event.code || event.error?.code || event.response?.error?.code || "incomplete terminal response";
|
||||
if (["response.incomplete", "response.failed", "error"].includes(event.type ?? "") || event.response?.error) {
|
||||
const code = event.code || event.error?.code || event.response?.error?.code || "incomplete terminal response";
|
||||
failure = [event.param, event.error?.param, event.response?.error?.param].includes("service_tier")
|
||||
? `${code} service_tier` : code;
|
||||
}
|
||||
await options?.onProviderStreamEvent?.(raw, model);
|
||||
},
|
||||
});
|
||||
|
||||
+2
-1
@@ -80,6 +80,7 @@ export interface ResolvedModelSpec {
|
||||
provider: string;
|
||||
id: string;
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
fastMode?: boolean;
|
||||
}
|
||||
|
||||
/** The first entry of the baked default order the model supports (owner,
|
||||
@@ -119,7 +120,7 @@ export function piEngineForSpec(spec: ResolvedModelSpec, background: boolean | (
|
||||
// the pin honest even when config or settings drift off the map.
|
||||
streamFn: (requestModel, context, options) => {
|
||||
const purpose = typeof background === "function" ? background : () => background;
|
||||
const stream = spec.provider === "chatgpt" ? chatGPTStream(purpose, lifecycle) : nativeId ? nativeSubscriptionStream(models.getProvider(nativeId)!, purpose, lifecycle) : models.streamSimple.bind(models);
|
||||
const stream = spec.provider === "chatgpt" ? chatGPTStream(purpose, lifecycle, spec.fastMode === true) : nativeId ? nativeSubscriptionStream(models.getProvider(nativeId)!, purpose, lifecycle) : models.streamSimple.bind(models);
|
||||
return stream(requestModel, context, { ...options, reasoning: level === "off" ? undefined : level });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -835,5 +835,11 @@
|
||||
"search engine is set to \"{v0}\" but its API key is missing — set the {v1} key": "search engine is set to \"{v0}\" but its API key is missing — set the {v1} key",
|
||||
"search engine: {v0} (chosen)": "search engine: {v0} (chosen)",
|
||||
"search engine: {v0} (auto)": "search engine: {v0} (auto)",
|
||||
"update": "update"
|
||||
"update": "update",
|
||||
"Fast": "Fast",
|
||||
"{v0} fast mode": "{v0} fast mode",
|
||||
"Fast mode uses more ChatGPT allowance.": "Fast mode uses more ChatGPT allowance.",
|
||||
"Fast mode is available only for ChatGPT models.": "Fast mode is available only for ChatGPT models.",
|
||||
"Could not save model settings": "Could not save model settings",
|
||||
"Fast mode is unavailable for this ChatGPT model or account. Turn off Fast mode or choose another model.": "Fast mode is unavailable for this ChatGPT model or account. Turn off Fast mode or choose another model."
|
||||
}
|
||||
|
||||
@@ -871,5 +871,11 @@
|
||||
"search engine is set to \"{v0}\" but its API key is missing — set the {v1} key": "Выбрана поисковая система «{v0}», но её API-ключ отсутствует — добавьте ключ {v1}",
|
||||
"search engine: {v0} (chosen)": "Поисковая система: {v0} (выбрана вручную)",
|
||||
"search engine: {v0} (auto)": "Поисковая система: {v0} (автоматически)",
|
||||
"update": "обновление"
|
||||
"update": "обновление",
|
||||
"Fast": "Быстро",
|
||||
"{v0} fast mode": "Быстрый режим: {v0}",
|
||||
"Fast mode uses more ChatGPT allowance.": "Быстрый режим быстрее расходует лимит ChatGPT.",
|
||||
"Fast mode is available only for ChatGPT models.": "Быстрый режим доступен только для моделей ChatGPT.",
|
||||
"Could not save model settings": "Не удалось сохранить настройки моделей",
|
||||
"Fast mode is unavailable for this ChatGPT model or account. Turn off Fast mode or choose another model.": "Быстрый режим недоступен для этой модели или учётной записи ChatGPT. Отключите быстрый режим или выберите другую модель."
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ export interface EffectiveSlot {
|
||||
provider: string;
|
||||
id: string;
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
fastMode?: boolean;
|
||||
}
|
||||
|
||||
/** A fully-resolved model pin for one role — same shape/level semantics
|
||||
@@ -61,6 +62,7 @@ export interface EffectiveModelSpec {
|
||||
provider: string;
|
||||
id: string;
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
fastMode?: boolean;
|
||||
}
|
||||
|
||||
/** The concrete appearance view — same shape as the factory's. */
|
||||
@@ -110,6 +112,8 @@ export function resolveConversationSpec(
|
||||
provider: model.provider,
|
||||
id: model.id,
|
||||
thinkingLevel: chat?.thinkingLevel ?? strong?.thinkingLevel,
|
||||
// Speed is a live tier preference, including for existing conversations.
|
||||
...(model.provider === "chatgpt" && strong?.provider === "chatgpt" && strong.fastMode === true ? { fastMode: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -131,7 +135,9 @@ export function resolveRoleSpec(role: ModelRole, record: UserSettings | null): E
|
||||
* to ride without the factory floor). */
|
||||
export function resolveSlot(slot: ModelSlot | undefined): EffectiveSlot | null {
|
||||
if (!slot?.model) return null;
|
||||
return { provider: slot.model.provider, id: slot.model.id, thinkingLevel: slot.thinkingLevel };
|
||||
return { provider: slot.model.provider, id: slot.model.id, thinkingLevel: slot.thinkingLevel,
|
||||
...(slot.model.provider === "chatgpt" && slot.fastMode === true ? { fastMode: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Resolve one role's spec — the BYOK chain (owner 2026-09-22): role
|
||||
@@ -153,6 +159,7 @@ export function resolveModelSpec(
|
||||
provider: model.provider,
|
||||
id: model.id,
|
||||
thinkingLevel: override?.thinkingLevel ?? slot?.thinkingLevel,
|
||||
...(model.provider === "chatgpt" && slot?.model?.provider === "chatgpt" && slot.fastMode === true ? { fastMode: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -193,6 +193,8 @@ export interface ModelSlot {
|
||||
model?: { provider: string; id: string };
|
||||
/** The thinking budget swap. */
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
/** Opt-in ChatGPT priority processing for this tier. Absent = off. */
|
||||
fastMode?: boolean;
|
||||
}
|
||||
|
||||
/** The `models` settings section (owner decision 2026-09-20: a dedicated
|
||||
@@ -490,7 +492,13 @@ function sanitizeModels(raw: Record<string, unknown>): ModelsSettings {
|
||||
const out: ModelsSettings = {};
|
||||
for (const slot of MODEL_SLOTS) {
|
||||
const cleaned = sanitizeRoleOverride(raw[slot]);
|
||||
if (cleaned) out[slot] = cleaned;
|
||||
if (cleaned) {
|
||||
const value = raw[slot] as Record<string, unknown>;
|
||||
out[slot] = {
|
||||
...cleaned,
|
||||
...(cleaned.model?.provider === "chatgpt" && typeof value.fastMode === "boolean" ? { fastMode: value.fastMode } : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ export function clampModelsLevels(settings: ModelsSettings): ModelsSettings {
|
||||
out[name] =
|
||||
slot.thinkingLevel !== undefined
|
||||
? {
|
||||
model: slot.model,
|
||||
...slot,
|
||||
thinkingLevel: clampLevelFor(slot.model.provider, slot.model.id, slot.thinkingLevel),
|
||||
}
|
||||
: slot;
|
||||
|
||||
@@ -348,7 +348,17 @@ export function parseModelsSettingsBody(body: unknown): SettingsBodyResult<Model
|
||||
if (!parsed.override.model) {
|
||||
return { ok: false, error: `slot "${key}" must set a model (thinkingLevel alone cannot run — absent slot = unset)` };
|
||||
}
|
||||
settings[key as (typeof MODEL_SLOTS)[number]] = parsed.override;
|
||||
const value = asRecord(raw[key])!;
|
||||
if (value.fastMode !== undefined && typeof value.fastMode !== "boolean") {
|
||||
return { ok: false, error: `${key}.fastMode must be a boolean` };
|
||||
}
|
||||
if (value.fastMode === true && parsed.override.model.provider !== "chatgpt") {
|
||||
return { ok: false, error: "Fast mode is available only for ChatGPT models." };
|
||||
}
|
||||
settings[key as (typeof MODEL_SLOTS)[number]] = {
|
||||
...parsed.override,
|
||||
...(typeof value.fastMode === "boolean" ? { fastMode: value.fastMode } : {}),
|
||||
};
|
||||
}
|
||||
return { ok: true, settings };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user