diff --git a/apps/desktop/src/renderer/locales/settings-provider-copy.ts b/apps/desktop/src/renderer/locales/settings-provider-copy.ts
index cb09b62e64..7a55ead23d 100644
--- a/apps/desktop/src/renderer/locales/settings-provider-copy.ts
+++ b/apps/desktop/src/renderer/locales/settings-provider-copy.ts
@@ -23,6 +23,10 @@ const zhCapabilitiesCopy = {
contextWindow: '上下文窗口(tokens)',
contextWindowHelp: '声明后压缩与预算按此值计算;留空跟随内置元数据。',
saveCapabilities: '保存能力声明',
+ fastMode: 'Fast 模式',
+ fastModeHelp: '使用 OpenAI 的 fast service tier;留空跟随服务商默认值。',
+ fastAuto: '自动',
+ fastEnabled: 'Fast',
};
const enCapabilitiesCopy = {
capabilities: 'Capabilities',
@@ -38,6 +42,10 @@ const enCapabilitiesCopy = {
contextWindow: 'Context window (tokens)',
contextWindowHelp: 'When set, compaction and budgets use this value; when empty, built-in metadata decides.',
saveCapabilities: 'Save capability declarations',
+ fastMode: 'Fast mode',
+ fastModeHelp: "Use OpenAI's fast service tier; empty follows the provider default.",
+ fastAuto: 'Auto',
+ fastEnabled: 'Fast',
};
const zhCopy = {
diff --git a/apps/desktop/src/renderer/settings/provider-connection-detail.tsx b/apps/desktop/src/renderer/settings/provider-connection-detail.tsx
index 886e273ecf..9dc89d7bdd 100644
--- a/apps/desktop/src/renderer/settings/provider-connection-detail.tsx
+++ b/apps/desktop/src/renderer/settings/provider-connection-detail.tsx
@@ -11,10 +11,11 @@ import {
Text,
VStack,
} from '@astryxdesign/core';
-import { PROVIDER_DEFAULTS } from '@maka/core/llm-connections';
+import { isRelayProviderType, PROVIDER_DEFAULTS } from '@maka/core/llm-connections';
import {
DECLARABLE_RELAY_THINKING_LEVELS,
THINKING_LEVELS,
+ supportsRelayFastServiceTier,
type RelayModelProfile,
type ThinkingLevel,
} from '@maka/core/model-thinking';
@@ -152,18 +153,19 @@ function ConnectionDetailInner(props: ConnectionDetailProps) {
setDraftThinkingLevels,
setDraftVision,
setDraftContextWindow,
+ setDraftServiceTier,
saveRelayProfiles,
runTest,
refreshModels,
remove,
refreshAfterRelogin,
} = useConnectionDetail(props);
- // Capability switches only exist for openai-compatible relays: built-in
+ // Capability switches only exist for custom OpenAI relays: built-in
// providers declare their thinking support in model metadata, a custom
// relay's backing model is unknown until the user says what it can do. The
// declaration is per model — a relay can front both a reasoner and a plain
// instruct model.
- const showsCapabilities = connection.providerType === 'openai-compatible';
+ const showsCapabilities = isRelayProviderType(connection.providerType);
// Rows are the enabled models, exactly — the store prunes a model's profile
// the moment it is disabled, so no declaration can ever belong to a row
// this list does not show. The editor edits the per-model draft; 保存
@@ -526,6 +528,11 @@ function ConnectionDetailInner(props: ConnectionDetailProps) {
? 'disabled'
: 'auto';
const draftLevels = declared?.thinkingLevels ?? [];
+ const serviceTierValue = declared?.serviceTier ?? 'auto';
+ const showsFastMode = supportsRelayFastServiceTier(
+ connection.providerType,
+ modelId,
+ );
// The menu offers the five declarable levels PLUS anything
// the stored table already claims — a level saved while it
// was still declarable (or hand-written into the document)
@@ -617,6 +624,25 @@ function ConnectionDetailInner(props: ConnectionDetailProps) {
}
/>
+ {showsFastMode && (
+
+
+ setDraftServiceTier(modelId, value === 'fast' ? 'fast' : undefined)
+ }
+ isDisabled={allActionsBusy}
+ />
+
+ )}
);
})}
diff --git a/apps/desktop/src/renderer/settings/use-connection-detail.ts b/apps/desktop/src/renderer/settings/use-connection-detail.ts
index 178ba45541..a1fa591664 100644
--- a/apps/desktop/src/renderer/settings/use-connection-detail.ts
+++ b/apps/desktop/src/renderer/settings/use-connection-detail.ts
@@ -336,7 +336,7 @@ export function useConnectionDetail(props: ConnectionDetailProps) {
}
}
- // Per-model profile declarations for openai-compatible relays, edited as a
+ // Per-model profile declarations for custom OpenAI relays, edited as a
// LOCAL DRAFT and committed by an explicit 保存 button — never keystroke by
// keystroke. A draft is `Record` seeded from the
// saved table; entries a user empties fully drop out of the map, and the
@@ -407,6 +407,17 @@ export function useConnectionDetail(props: ConnectionDetailProps) {
});
}
+ function setDraftServiceTier(modelId: string, serviceTier: 'fast' | undefined): void {
+ updateRelayProfileDraft(modelId, (current) => {
+ if (serviceTier === undefined) {
+ if (!current) return current;
+ const { serviceTier: _dropped, ...rest } = current;
+ return Object.keys(rest).length > 0 ? rest : undefined;
+ }
+ return { ...(current ?? {}), serviceTier };
+ });
+ }
+
// Compare against what persistence would store: drafts pruned to the
// current selection and order-normalized by the same sanitizer the write
// path applies, so a reordered-but-equal draft doesn't keep 保存 lit.
@@ -641,6 +652,7 @@ export function useConnectionDetail(props: ConnectionDetailProps) {
setDraftThinkingLevels,
setDraftVision,
setDraftContextWindow,
+ setDraftServiceTier,
saveRelayProfiles,
runTest,
refreshModels,
diff --git a/packages/core/src/__tests__/model-thinking.test.ts b/packages/core/src/__tests__/model-thinking.test.ts
index a136a28808..b1b98e7d42 100644
--- a/packages/core/src/__tests__/model-thinking.test.ts
+++ b/packages/core/src/__tests__/model-thinking.test.ts
@@ -9,7 +9,9 @@ import {
thinkingOptionsForModel,
thinkingVariantsForConnection,
thinkingVariantsForModel,
+ supportsRelayFastServiceTier,
} from '../model-thinking.js';
+import { isRelayProviderType } from '../llm-connections.js';
test('declarable relay levels are every intensity tier but off', () => {
// `off` is a disable-wire encoding (reasoning_effort 'none'), not an
@@ -25,6 +27,31 @@ test('declarable relay levels are every intensity tier but off', () => {
assert.deepEqual([...thinkingVariantsForConnection(declaredOff, 'm')], ['low']);
});
+test('relay profiles preserve the fast service tier declaration', () => {
+ assert.deepEqual(normalizeRelayModelProfiles({ m: { serviceTier: 'fast' } }), {
+ m: { serviceTier: 'fast' },
+ });
+ assert.deepEqual(normalizeRelayModelProfiles({ m: { serviceTier: 'unknown' } }), undefined);
+});
+
+test('Fast visibility mirrors the pinned OpenAI SDK priority-processing families', () => {
+ const cases = [
+ ['gpt-4o', true],
+ ['gpt-4.1', true],
+ ['gpt-5', true],
+ ['gpt-5.1', true],
+ ['gpt-5-nano', false],
+ ['gpt-5-chat-latest', false],
+ ['o3-mini', true],
+ ['o4-mini', true],
+ ['plain-relay-id', false],
+ ] as const;
+ for (const [modelId, expected] of cases) {
+ assert.equal(supportsRelayFastServiceTier('openai-responses-compatible', modelId), expected);
+ assert.equal(supportsRelayFastServiceTier('openai-compatible', modelId), false);
+ }
+});
+
test('relayModelProfile returns undefined without a usable declaration', () => {
const connection = {
providerType: 'openai-compatible',
@@ -78,18 +105,32 @@ test('relayModelProfile normalizes order, keeps explicit vision:false, and bound
}
});
-test('relayModelProfile gates declarations to openai-compatible relays', () => {
+test('relayModelProfile gates declarations to custom OpenAI relays', () => {
const profiles = { m: { vision: true, contextWindow: 64_000 } };
assert.deepEqual(
relayModelProfile({ providerType: 'openai-compatible', relayModelProfiles: profiles }, 'm'),
{ vision: true, contextWindow: 64_000 },
);
+ assert.deepEqual(
+ relayModelProfile(
+ { providerType: 'openai-responses-compatible', relayModelProfiles: profiles },
+ 'm',
+ ),
+ { vision: true, contextWindow: 64_000 },
+ );
// The same table on a non-relay connection is inert: metadata rules.
for (const providerType of ['anthropic', 'openai'] as const) {
assert.equal(relayModelProfile({ providerType, relayModelProfiles: profiles }, 'm'), undefined);
}
});
+test('isRelayProviderType only accepts the two custom OpenAI relay providers', () => {
+ assert.equal(isRelayProviderType('openai-compatible'), true);
+ assert.equal(isRelayProviderType('openai-responses-compatible'), true);
+ assert.equal(isRelayProviderType('openai'), false);
+ assert.equal(isRelayProviderType('anthropic'), false);
+});
+
test('normalizeRelayModelProfiles sanitizes write-side tables', () => {
const sanitized = normalizeRelayModelProfiles({
reasoner: { thinkingLevels: ['high', 'low', 'turbo'], vision: true, contextWindow: 200_000 },
diff --git a/packages/core/src/__tests__/runtime-policy-codec.test.ts b/packages/core/src/__tests__/runtime-policy-codec.test.ts
index e39d69eb08..961db43d62 100644
--- a/packages/core/src/__tests__/runtime-policy-codec.test.ts
+++ b/packages/core/src/__tests__/runtime-policy-codec.test.ts
@@ -222,6 +222,7 @@ test('relay model profiles round-trip canonical entries and drafts, strictly', (
thinkingLevels: ['minimal', 'low'],
vision: true,
contextWindow: 128_000,
+ serviceTier: 'fast',
},
};
const draft = normalizeCreateCatalogConnectionInput({
@@ -237,6 +238,19 @@ test('relay model profiles round-trip canonical entries and drafts, strictly', (
},
});
assert.deepEqual(draft.connection.relayModelProfiles, table);
+ const responsesDraft = normalizeCreateCatalogConnectionInput({
+ expectedCatalogRevision: 0,
+ connection: {
+ slug: 'responses-relay',
+ name: 'Responses Relay',
+ providerType: 'openai-responses-compatible',
+ baseUrl: 'https://responses.example/v1',
+ enabled: true,
+ enabledModelIds: ['relay-reasoner'],
+ relayModelProfiles: table,
+ },
+ });
+ assert.deepEqual(responsesDraft.connection.relayModelProfiles, table);
// The canonical path re-decodes the same table (entry = draft + identity).
const entry = decodeCanonicalConnectionCatalogEntry({
...draft.connection,
diff --git a/packages/core/src/llm-connections.ts b/packages/core/src/llm-connections.ts
index 66653b63ca..821e46b0d9 100644
--- a/packages/core/src/llm-connections.ts
+++ b/packages/core/src/llm-connections.ts
@@ -49,6 +49,12 @@ export type {
ProviderType,
};
+export function isRelayProviderType(
+ providerType: ProviderType,
+): providerType is 'openai-compatible' | 'openai-responses-compatible' {
+ return PROVIDER_REGISTRY[providerType].relayModelProfiles === true;
+}
+
export type ConnectionAuth =
| { kind: 'api_key'; apiKey: string }
| { kind: 'optional_api_key'; apiKey?: string }
@@ -107,13 +113,13 @@ export interface RuntimeExecutionConnection {
defaultModel: string;
models?: ModelInfo[];
/**
- * Per-model user declarations for an `openai-compatible` relay: the facts
+ * Per-model user declarations for a custom OpenAI relay: the facts
* (offered thinking levels, vision enable/disable, context window) that
* neither the relay's /models report nor built-in metadata can decide
* (see `RelayModelProfile` in `model-thinking.ts`). First-class and typed —
* relay models are unknown to metadata and a catalog refresh rewrites
* `models[]` rows, so declarations live next to the user-edited fields.
- * Invariants enforced at store boundaries: only `openai-compatible`
+ * Invariants enforced at store boundaries: only custom OpenAI relay
* connections carry profiles, and only for ids in `enabledModelIds`
* (disabling a model deletes its profile).
*/
@@ -624,7 +630,7 @@ export interface UpdateConnectionInput {
/**
* Replace the whole relay profiles table: absent leaves it untouched,
* `null` clears it outright, a table replaces it (with the usual rules —
- * only `openai-compatible`, only for `enabledModelIds`).
+ * only custom OpenAI relays, only for `enabledModelIds`).
*/
relayModelProfiles?: RelayModelProfiles | null;
requestBodyOverlay?: JsonObject | null;
diff --git a/packages/core/src/model-thinking.ts b/packages/core/src/model-thinking.ts
index 36f7938ae3..cc9b4ee215 100644
--- a/packages/core/src/model-thinking.ts
+++ b/packages/core/src/model-thinking.ts
@@ -17,6 +17,7 @@
* per-model supported set, so the UI and runtime share one source of truth.
*/
+import { isRelayProviderType } from './llm-connections.js';
import type { ProviderType } from './llm-connections.js';
import { lookupModelMetadata } from './model-metadata.js';
@@ -101,7 +102,8 @@ export function deriveThinkingChoices(
}
/**
- * One model behind an `openai-compatible` relay, as declared by the user:
+ * One model behind an OpenAI-compatible relay (Chat Completions or Responses),
+ * as declared by the user:
* the facts neither the relay's /models report nor built-in metadata can be
* trusted to know. Every field is independent, and every ABSENT field means
* "Auto" — the /models report and the metadata chain decide. The single
@@ -113,14 +115,16 @@ export function deriveThinkingChoices(
* (`relayModelProfiles`, keyed by model id): relay models are unknown to
* `model-metadata.ts` and a catalog refresh rewrites `models[]` rows, so
* declarations sit next to the user-edited connection fields. Two invariants
- * are enforced at the store boundaries — profiles exist only for
- * `openai-compatible` connections, and only for models in `enabledModelIds`
+ * are enforced at the store boundaries — profiles exist only for custom OpenAI
+ * relay connections, and only for models in `enabledModelIds`
* (disabling a model deletes its profile).
*/
export interface RelayModelProfile {
readonly thinkingLevels?: readonly ThinkingLevel[];
readonly vision?: boolean;
readonly contextWindow?: number;
+ /** Use OpenAI's low-latency service tier for this relay model. */
+ readonly serviceTier?: 'fast';
}
export type RelayModelProfiles = Readonly>;
@@ -135,6 +139,7 @@ function normalizeRelayModelProfile(entry: unknown): RelayModelProfile | undefin
thinkingLevels?: readonly ThinkingLevel[];
vision?: boolean;
contextWindow?: number;
+ serviceTier?: 'fast';
} = {};
if (Array.isArray(entry.thinkingLevels)) {
// Declared levels are filtered to the declarable vocabulary, not merely
@@ -166,6 +171,7 @@ function normalizeRelayModelProfile(entry: unknown): RelayModelProfile | undefin
) {
declared.contextWindow = entry.contextWindow;
}
+ if (entry.serviceTier === 'fast') declared.serviceTier = 'fast';
return Object.keys(declared).length > 0 ? (declared as RelayModelProfile) : undefined;
}
@@ -231,12 +237,31 @@ export function relayModelProfile(
connection: ConnectionThinkingContext,
modelId: string,
): RelayModelProfile | undefined {
- if (connection.providerType !== 'openai-compatible') return undefined;
+ if (!isRelayProviderType(connection.providerType)) return undefined;
return normalizeRelayModelProfile(connection.relayModelProfiles?.[modelId]);
}
/**
- * `openai-compatible` connections declare thinking support **per model** via
+ * Mirrors @ai-sdk/openai@4.0.42 priority-processing detection. The UI and
+ * runtime share this gate so a saved Fast declaration always reaches the wire.
+ */
+export function supportsRelayFastServiceTier(providerType: ProviderType, modelId: string): boolean {
+ if (providerType !== 'openai-responses-compatible') return false;
+ const oSeriesVersion = /^o(\d+)(?:-|$)/.exec(modelId)?.[1];
+ const gptMatch = /^gpt-(\d+)(?:\.(\d+))?(?:-(.+))?$/.exec(modelId);
+ const gptMajor = gptMatch?.[1] === undefined ? undefined : Number(gptMatch[1]);
+ const gptVariant = gptMatch?.[3];
+ const isGptNanoModel = gptVariant?.startsWith('nano') ?? false;
+ const isGptChatModel = gptVariant?.startsWith('chat') ?? false;
+ return (
+ modelId.startsWith('gpt-4') ||
+ (gptMajor !== undefined && gptMajor >= 5 && !isGptNanoModel && !isGptChatModel) ||
+ (oSeriesVersion !== undefined && Number(oSeriesVersion) >= 3)
+ );
+}
+
+/**
+ * OpenAI-compatible relay connections declare thinking support **per model** via
* `relayModelProfiles[modelId].thinkingLevels` — a relay may front a
* DeepSeek-family reasoner and a plain instruct model side by side, so the
* declaration granularity is the model, not the connection. Without a usable
diff --git a/packages/core/src/provider-registry.ts b/packages/core/src/provider-registry.ts
index e4f5d24abf..a4b3c99c00 100644
--- a/packages/core/src/provider-registry.ts
+++ b/packages/core/src/provider-registry.ts
@@ -83,6 +83,8 @@ export interface ProviderDefaults {
status: 'ready' | 'phase3-experimental';
protocol: 'anthropic' | 'openai' | 'google' | 'cohere';
runtimeAdapter: ProviderRuntimeAdapter;
+ /** User-declared per-model capabilities are authoritative for this provider. */
+ relayModelProfiles?: boolean;
modelDiscovery: ProviderModelDiscovery;
category: ProviderCategory;
catalogGroup?: ProviderCatalogGroup;
@@ -1727,6 +1729,7 @@ const providerRegistry = {
status: 'ready',
protocol: 'openai',
runtimeAdapter: { kind: 'openai-compatible', name: 'connection', requireBaseUrl: true },
+ relayModelProfiles: true,
modelDiscovery: { kind: 'protocol' },
category: 'custom',
catalogGroup: 'aggregators',
@@ -1745,6 +1748,7 @@ const providerRegistry = {
status: 'ready',
protocol: 'openai',
runtimeAdapter: { kind: 'openai', apiProtocol: 'openai-responses' },
+ relayModelProfiles: true,
modelDiscovery: { kind: 'protocol' },
category: 'custom',
catalogGroup: 'aggregators',
diff --git a/packages/core/src/runtime-policy/connection-catalog-codec.ts b/packages/core/src/runtime-policy/connection-catalog-codec.ts
index 306b8e184c..7f776916c4 100644
--- a/packages/core/src/runtime-policy/connection-catalog-codec.ts
+++ b/packages/core/src/runtime-policy/connection-catalog-codec.ts
@@ -1,4 +1,5 @@
import {
+ isRelayProviderType,
PROVIDER_DEFAULTS,
providerDefaultsOf,
validateSlug,
@@ -208,12 +209,12 @@ export function normalizeConnectionCatalogEntryUpdateForProvider(
};
}
-// Relay profiles are an openai-compatible feature: on any other provider the
+// Relay profiles are a custom OpenAI relay feature: on any other provider the
// metadata chain is the truth, and a table here would either sit as dead
// state or silently shadow metadata for the ungated read seams.
function rejectForeignProfiles(providerType: ProviderType): void {
- if (providerType !== 'openai-compatible') {
- throw domainError('relay model profiles are only supported for openai-compatible connections');
+ if (!isRelayProviderType(providerType)) {
+ throw domainError('relay model profiles are only supported for OpenAI-compatible connections');
}
}
@@ -247,13 +248,14 @@ export function decodeRelayModelProfilesTable(
const entry = exactRecord(
rawEntry,
`relay model profile for ${modelId}`,
- ['thinkingLevels', 'vision', 'contextWindow'],
+ ['thinkingLevels', 'vision', 'contextWindow', 'serviceTier'],
[],
);
const declared: {
thinkingLevels?: readonly ThinkingLevel[];
vision?: boolean;
contextWindow?: number;
+ serviceTier?: 'fast';
} = {};
if (entry.thinkingLevels !== undefined) {
if (!Array.isArray(entry.thinkingLevels) || entry.thinkingLevels.length === 0) {
@@ -286,6 +288,12 @@ export function decodeRelayModelProfilesTable(
Number.MAX_SAFE_INTEGER,
);
}
+ if (entry.serviceTier !== undefined) {
+ if (entry.serviceTier !== 'fast') {
+ throw domainError(`declared service tier for ${modelId} must be fast`);
+ }
+ declared.serviceTier = 'fast';
+ }
if (Object.keys(declared).length === 0) {
throw domainError(`relay model profile for ${modelId} declares nothing`);
}
diff --git a/packages/runtime-host/src/protocol/index.ts b/packages/runtime-host/src/protocol/index.ts
index 54bc6f661b..b18b69ab14 100644
--- a/packages/runtime-host/src/protocol/index.ts
+++ b/packages/runtime-host/src/protocol/index.ts
@@ -72,7 +72,9 @@ export const RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION = 1 as const;
export const RUNTIME_HOST_PROTOCOL_VERSION = 0 as const;
// Increment when the same protocol version no longer guarantees safe Client-Host
// interoperability. Mismatches are rejected before domain commands are admitted.
-export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 27 as const;
+export const RUNTIME_HOST_COMPATIBILITY_EPOCH = 28 as const;
+// 28: Relay model profiles carry the Fast service-tier declaration. Older
+// peers cannot safely preserve that Runtime Policy field.
// 27: Runtime Policy carries the Host-owned shell preference used by tool,
// PTY, and prompt composition. Older peers cannot safely preserve that field.
// Transcript pages amortize storage and network round trips with a 512 KiB raw
diff --git a/packages/runtime/src/__tests__/model-factory-thinking.test.ts b/packages/runtime/src/__tests__/model-factory-thinking.test.ts
index 919ac5e78c..59da1f9f86 100644
--- a/packages/runtime/src/__tests__/model-factory-thinking.test.ts
+++ b/packages/runtime/src/__tests__/model-factory-thinking.test.ts
@@ -547,6 +547,107 @@ describe('buildProviderOptions: openai-compatible namespace', () => {
);
});
+ test('custom Responses relays use per-model declared levels on the Responses wire', () => {
+ const declared: LlmConnection = {
+ ...conn('openai-responses-compatible', 'my-responses-relay'),
+ baseUrl: 'https://relay.example/v1',
+ models: [{ id: 'custom-reasoner', apiProtocol: 'openai-responses' }],
+ relayModelProfiles: {
+ 'custom-reasoner': { thinkingLevels: ['minimal', 'low', 'medium', 'high', 'max'] },
+ },
+ };
+ assert.deepEqual(buildProviderOptions(declared, 'custom-reasoner', 'high'), {
+ openai: { store: false, forceReasoning: true, reasoningEffort: 'high' },
+ });
+ assert.deepEqual(buildProviderOptions(declared, 'custom-reasoner', 'max'), {
+ openai: { store: false, forceReasoning: true, reasoningEffort: 'max' },
+ });
+ assert.deepEqual(buildProviderOptions(declared, 'custom-reasoner', 'xhigh'), {
+ openai: { store: false, forceReasoning: true },
+ });
+ });
+
+ test('custom relays send the declared fast service tier independently of reasoning', () => {
+ const chat: LlmConnection = {
+ ...conn('openai-compatible', 'my-relay'),
+ baseUrl: 'https://relay.example/v1',
+ relayModelProfiles: { 'fast-model': { serviceTier: 'fast' } },
+ };
+ assert.deepEqual(buildProviderOptions(chat, 'fast-model'), {});
+ const responses: LlmConnection = {
+ ...conn('openai-responses-compatible', 'my-responses-relay'),
+ baseUrl: 'https://relay.example/v1',
+ models: [{ id: 'gpt-5-relay', apiProtocol: 'openai-responses' }],
+ relayModelProfiles: { 'gpt-5-relay': { serviceTier: 'fast' } },
+ };
+ assert.deepEqual(buildProviderOptions(responses, 'gpt-5-relay'), {
+ openai: { store: false, forceReasoning: true, serviceTier: 'fast' },
+ });
+ assert.deepEqual(
+ buildProviderOptions(
+ { ...conn('openai-compatible', 'my-relay'), baseUrl: 'https://relay.example/v1' },
+ 'fast-model',
+ ),
+ {},
+ );
+ });
+
+ test('Fast provider options mirror the pinned OpenAI SDK model gate', () => {
+ const cases = [
+ ['gpt-4o', true],
+ ['gpt-4.1', true],
+ ['gpt-5', true],
+ ['gpt-5.1', true],
+ ['gpt-5-nano', false],
+ ['gpt-5-chat-latest', false],
+ ['o3-mini', true],
+ ['o4-mini', true],
+ ['plain-relay-id', false],
+ ] as const;
+ for (const [modelId, supported] of cases) {
+ const connection: LlmConnection = {
+ ...conn('openai-responses-compatible', 'my-responses-relay'),
+ baseUrl: 'https://relay.example/v1',
+ models: [{ id: modelId, apiProtocol: 'openai-responses' }],
+ relayModelProfiles: { [modelId]: { serviceTier: 'fast' } },
+ };
+ assert.deepEqual(buildProviderOptions(connection, modelId), {
+ openai: {
+ store: false,
+ forceReasoning: true,
+ ...(supported ? { serviceTier: 'fast' } : {}),
+ },
+ });
+ }
+ });
+
+ test('Fast reaches the Responses request body for an OpenAI-named relay model', async () => {
+ const bodies: Record[] = [];
+ const modelConnection: LlmConnection = {
+ ...conn('openai-responses-compatible', 'my-responses-relay'),
+ baseUrl: 'https://relay.example/v1',
+ models: [{ id: 'gpt-5-relay', apiProtocol: 'openai-responses' }],
+ relayModelProfiles: { 'gpt-5-relay': { serviceTier: 'fast' } },
+ };
+ const model = getAIModel({
+ connection: modelConnection,
+ apiKey: 'relay-key',
+ modelId: 'gpt-5-relay',
+ fetch: async (_input, init) => {
+ bodies.push(JSON.parse(String(init?.body)) as Record);
+ return new Response(
+ JSON.stringify({ id: 'resp-1', object: 'response', model: 'gpt-5-relay', output: [] }),
+ { status: 200, headers: { 'content-type': 'application/json' } },
+ );
+ },
+ });
+ await model.doGenerate({
+ prompt: [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }],
+ providerOptions: buildProviderOptions(modelConnection, 'gpt-5-relay'),
+ });
+ assert.equal(bodies[0]?.service_tier, 'fast');
+ });
+
test('declared relay levels reach the actual chat-completions request body', async () => {
// Intermediate providerOptions objects matching does not prove the wire
// carries the effort — this capture asserts the SDK's camelCase slug key
diff --git a/packages/runtime/src/model-factory.ts b/packages/runtime/src/model-factory.ts
index f6dc871cfb..15c8946a52 100644
--- a/packages/runtime/src/model-factory.ts
+++ b/packages/runtime/src/model-factory.ts
@@ -17,6 +17,7 @@ import type { ProviderRuntimeAdapter } from '@maka/core/llm-connections';
import type { ThinkingLevel } from '@maka/core/model-thinking';
import {
resolveThinkingLevel,
+ supportsRelayFastServiceTier,
thinkingOptionsForModel,
thinkingVariantsForConnection,
type ThinkingOptions,
@@ -522,6 +523,13 @@ function buildFamilyWire(
): SharedV4ProviderOptions {
const { adapter, wire, reasoningReplay } = resolveModelRuntime(connection, modelId);
const reasoningEffort = level ? (level === 'off' ? 'none' : level) : undefined;
+ const serviceTier =
+ wire === 'openai-responses' &&
+ reasoningReplay.kind === 'responses' &&
+ reasoningReplay.contract.adapter === 'openai' &&
+ supportsRelayFastServiceTier(connection.providerType, modelId)
+ ? connection.relayModelProfiles?.[modelId]?.serviceTier
+ : undefined;
// Provider selection and reasoning continuation are independent. The OpenAI
// provider reads its provider-options namespace; the Open Responses provider
// consumes a provider-native reasoningEffort through the same namespace,
@@ -539,8 +547,13 @@ function buildFamilyWire(
// sends `xhigh` to high, not max). The SDK resolves providerOptions
// under the raw provider `name` — no camelCase alias, unlike
// openai-compatible — so key by the same name getAIModel passes.
- return reasoningEffort
- ? { [openAiCompatibleProviderName(adapter, connection)]: { reasoningEffort } }
+ return reasoningEffort || serviceTier
+ ? {
+ [openAiCompatibleProviderName(adapter, connection)]: {
+ ...(reasoningEffort ? { reasoningEffort } : {}),
+ ...(serviceTier ? { serviceTier } : {}),
+ },
+ }
: {};
}
return {
@@ -550,17 +563,24 @@ function buildFamilyWire(
? { forceReasoning: true }
: {}),
...(reasoningEffort ? { reasoningEffort } : {}),
+ ...(serviceTier ? { serviceTier } : {}),
},
};
}
- if (!reasoningEffort) return {};
+ if (!reasoningEffort && !serviceTier) return {};
switch (adapter.kind) {
case 'openai-compatible':
return {
- [openAiCompatibleProviderOptionsKey(adapter, connection)]: { reasoningEffort },
+ [openAiCompatibleProviderOptionsKey(adapter, connection)]: {
+ ...(reasoningEffort ? { reasoningEffort } : {}),
+ },
};
case 'openai':
- return { openai: { reasoningEffort } };
+ return {
+ openai: {
+ ...(reasoningEffort ? { reasoningEffort } : {}),
+ },
+ };
case 'anthropic':
// Anthropic-protocol models declare no `none` effort, so an off
// choice only exists where an explicit case wires it.