Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions electron/assets/providers/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -6613,6 +6613,207 @@
}
]
},
{
"id": "meta",
"displayName": "Meta",
"supportedAuthMethods": [
"api-key",
"environment"
],
"supportedProtocols": [
"openai-responses"
],
"allowsCustomModels": true,
"lifecycle": "active",
"provenance": {
"source": "models.dev",
"observedAt": "2026-08-12T03:40:23.755Z"
},
"models": [
{
"id": "muse-spark-1.1",
"displayName": "Muse Spark 1.1",
"protocol": "openai-responses",
"capabilities": {
"inputModalities": [
"text",
"image",
"pdf",
"video"
],
"outputModalities": [
"text"
],
"tools": true,
"reasoning": true
},
"reasoningLevels": [
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"reasoningDefault": "medium",
"limits": {
"contextTokens": 1000000,
"outputTokens": 32000
},
"lifecycle": "active",
"pricing": {
"currency": "USD",
"effectiveAt": "2026-08-12T03:40:23.755Z",
"rates": {
"input": {
"amount": "1.25",
"per": 1000000,
"unit": "tokens"
},
"output": {
"amount": "4.25",
"per": 1000000,
"unit": "tokens"
},
"cacheRead": {
"amount": "0.15",
"per": 1000000,
"unit": "tokens"
}
},
"provenance": {
"source": "models.dev",
"observedAt": "2026-08-12T03:40:23.755Z"
}
},
"provenance": {
"source": "models.dev",
"observedAt": "2026-08-12T03:40:23.755Z"
}
},
{
"id": "muse-spark-1.2",
"displayName": "Muse Spark 1.2",
"protocol": "openai-responses",
"capabilities": {
"inputModalities": [
"text",
"image",
"video",
"pdf",
"audio"
],
"outputModalities": [
"text"
],
"tools": true,
"reasoning": true
},
"reasoningLevels": [
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"reasoningDefault": "medium",
"limits": {
"contextTokens": 1048576,
"outputTokens": 131072
},
"lifecycle": "active",
"pricing": {
"currency": "USD",
"effectiveAt": "2026-08-12T03:40:23.755Z",
"rates": {
"input": {
"amount": "1.25",
"per": 1000000,
"unit": "tokens"
},
"output": {
"amount": "4.25",
"per": 1000000,
"unit": "tokens"
},
"cacheRead": {
"amount": "0.15",
"per": 1000000,
"unit": "tokens"
}
},
"provenance": {
"source": "models.dev",
"observedAt": "2026-08-12T03:40:23.755Z"
}
},
"provenance": {
"source": "models.dev",
"observedAt": "2026-08-12T03:40:23.755Z"
}
},
{
"id": "muse-spark-1.2-contributor",
"displayName": "Muse Spark 1.2 Contributor",
"protocol": "openai-responses",
"capabilities": {
"inputModalities": [
"text",
"image",
"video",
"pdf",
"audio"
],
"outputModalities": [
"text"
],
"tools": true,
"reasoning": true
},
"reasoningLevels": [
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"reasoningDefault": "medium",
"limits": {
"contextTokens": 1048576,
"outputTokens": 131072
},
"lifecycle": "active",
"pricing": {
"currency": "USD",
"effectiveAt": "2026-08-12T03:40:23.755Z",
"rates": {
"input": {
"amount": "0.1",
"per": 1000000,
"unit": "tokens"
},
"output": {
"amount": "0.2",
"per": 1000000,
"unit": "tokens"
},
"cacheRead": {
"amount": "0.002",
"per": 1000000,
"unit": "tokens"
}
},
"provenance": {
"source": "models.dev",
"observedAt": "2026-08-12T03:40:23.755Z"
}
},
"provenance": {
"source": "models.dev",
"observedAt": "2026-08-12T03:40:23.755Z"
}
}
]
},
{
"id": "generic-openai-compatible",
"displayName": "Generic OpenAI-compatible",
Expand Down
1 change: 1 addition & 0 deletions electron/scripts/provider-catalog/seed-models-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const PROVIDERS = [
{ sourceId: 'opencode-go', id: 'opencode-go', protocol: 'openai-compatible', auth: ['api-key', 'environment'] },
{ sourceId: 'lilac', id: 'lilac', protocol: 'openai-compatible', auth: ['api-key', 'environment'] },
{ sourceId: 'neuralwatt', id: 'neuralwatt', protocol: 'openai-compatible', auth: ['api-key', 'environment'] },
{ sourceId: 'meta', id: 'meta', protocol: 'openai-responses', auth: ['api-key', 'environment'] },
];

/** Generic providers are Orchid-owned definitions, not models.dev providers. */
Expand Down
3 changes: 2 additions & 1 deletion electron/src/main/agents/subagent-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type { ThinkingReplayContext } from '../llm/history';
import type { CacheFacet, ThinkingPolicy } from '../../shared/types/provider-facets';
import { resolveSubagentTier } from '../providers/facets/tiers';
import { assembleFacetProviderOptions } from '../providers/facets/turn-options';
import type { ModelSelection } from '../../shared/types/provider';
import type { ModelSelection, ProviderProtocol } from '../../shared/types/provider';
import { streamChat, type StreamEvent } from '../llm/orchestrator';
import { resolveSubagentEffort } from '../llm/reasoning-effort';
import { getConfig } from '../config/loader';
Expand Down Expand Up @@ -281,6 +281,7 @@ export function createSubagentStreamRunner(): SubagentStreamRunner {
thinkingReplay: {
policy: thinkingPolicy ?? DEFAULT_THINKING_POLICY,
selection: { providerId: providerSnapshot.providerId, modelId: selection.modelId },
protocol: providerSnapshot.protocol as ProviderProtocol,
} satisfies ThinkingReplayContext,
cachePlacement: cacheFacet
? { facet: cacheFacet, ttl: cacheTtl, sessionKey: cacheSessionKey }
Expand Down
2 changes: 2 additions & 0 deletions electron/src/main/ipc/chat/send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from '../../providers/facets/thinking';
import type { ThinkingReplayContext } from '../../llm/history';
import type { CacheFacet, ThinkingPolicy } from '../../../shared/types/provider-facets';
import type { ProviderProtocol } from '../../../shared/types/provider';
import { resolveMainAgentTier } from '../../providers/facets/tiers';
import { assembleFacetProviderOptions } from '../../providers/facets/turn-options';
import { getSessionManager } from '../../session/singleton';
Expand Down Expand Up @@ -176,6 +177,7 @@ export async function startChatTurn(
const thinkingReplay: ThinkingReplayContext = {
policy: thinkingPolicy ?? DEFAULT_THINKING_POLICY,
selection: { providerId: providerSnapshot.providerId, modelId: turnSelection.modelId },
protocol: providerSnapshot.protocol as ProviderProtocol,
};
const agent = agents.find((candidate) => candidate.name === 'general') ?? agents[0] ?? {
name: 'general', type: 'subagent' as const, tier: 'bloom' as const,
Expand Down
7 changes: 7 additions & 0 deletions electron/src/main/llm/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
} from '../../shared/types/message';
import { MessageType, MessageRole, messageToApiFormat } from '../../shared/types/message';
import type { ThinkingPolicy } from '../../shared/types/provider-facets';
import type { ProviderProtocol } from '../../shared/types/provider';
import {
buildThinkingProviderOptions,
decideThinkingReplay,
Expand All @@ -36,6 +37,12 @@ import {
export interface ThinkingReplayContext {
readonly policy: ThinkingPolicy;
readonly selection: ThinkingReplayIdentity;
/**
* Frozen model protocol. Responses-protocol models enforce reasoning-item
* ordering at replay (a reasoning item must be followed by an assistant
* message or function_call, or the provider rejects the input).
*/
readonly protocol?: ProviderProtocol;
}

function thinkingReplayPart(
Expand Down
58 changes: 55 additions & 3 deletions electron/src/main/llm/model-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,18 @@
* Model message conversion — OpenAI-shaped replay records to AI SDK input.
*
* `history.ts` owns replay/pairing semantics. This adapter owns only the
* provider-neutral shape conversion required by AI SDK's `streamText`.
* provider-neutral shape conversion required by AI SDK's `streamText`, plus
* two Responses-protocol replay rules:
*
* - Commentary phase: assistant text that precedes tool calls in the same
* message is intermediate commentary, so it replays with
* `openai.phase: "commentary"` for the Responses adapter (Meta rejects a
* plain final-answer message immediately before a function_call).
* - Reasoning ordering: a replayed reasoning item must be followed by an
* assistant message or a function_call before the next user/system/developer
* message (Meta returns HTTP 400 otherwise). Reasoning-only turns — a
* cancelled turn or a text-less artifact — get a minimal assistant message
* after the reasoning item, the workaround the provider documents.
*/
import type { AssistantContent, ModelMessage } from 'ai';
import {
Expand All @@ -23,7 +34,10 @@ function toTextOnlyContent(content: ApiMessage['content']): string {
* System messages deliberately do not appear in the returned array: callers
* pass the composed system prompt through `streamText`'s `system` parameter.
*/
export function toModelMessages(historyMessages: readonly ApiMessage[]): ModelMessage[] {
export function toModelMessages(
historyMessages: readonly ApiMessage[],
options: { readonly responsesReplay?: boolean } = {},
): ModelMessage[] {
const modelMessages: ModelMessage[] = [];

for (const message of historyMessages) {
Expand All @@ -32,6 +46,7 @@ export function toModelMessages(historyMessages: readonly ApiMessage[]): ModelMe
}

if (message.role === MessageRole.ASSISTANT) {
const hasToolCalls = (message.tool_calls?.length ?? 0) > 0;
const contentArray = Array.isArray(message.content)
? message.content.map((part) => {
if (part.type === 'reasoning') {
Expand All @@ -43,7 +58,15 @@ export function toModelMessages(historyMessages: readonly ApiMessage[]): ModelMe
...(part.providerOptions ? { providerOptions: part.providerOptions } : {}),
};
}
return { type: 'text' as const, text: part.text };
// Text emitted before tool calls in the same message is
// intermediate commentary (Meta requires phase: "commentary" on
// such replayed messages; a plain final answer 400s).
const commentary = options.responsesReplay && hasToolCalls;
return {
type: 'text' as const,
text: part.text,
...(commentary ? { providerOptions: { openai: { phase: 'commentary' } } } : {}),
};
})
: message.content
? [{ type: 'text' as const, text: message.content }]
Expand Down Expand Up @@ -106,5 +129,34 @@ export function toModelMessages(historyMessages: readonly ApiMessage[]): ModelMe
}
}

if (options.responsesReplay) {
insertMinimalAssistantAfterReasoningOnly(modelMessages);
}

return modelMessages;
}

/**
* Responses input validation requires every reasoning item to be followed by
* an assistant message or a function_call before the next user/system/developer
* message. Orchid replays each THINKING message as its own assistant reasoning
* message, so a reasoning-only turn (cancelled mid-thought, or a text-less
* opaque artifact) would leave `reasoning` directly before a user message and
* be rejected. The provider-documented workaround is a minimal assistant
* message inserted after the reasoning item.
*/
function insertMinimalAssistantAfterReasoningOnly(messages: ModelMessage[]): void {
for (let index = 0; index < messages.length; index += 1) {
const message = messages[index];
const parts = Array.isArray(message.content) ? message.content : [];
const hasReasoning = parts.some((part) => part.type === 'reasoning');
if (!hasReasoning) continue;
const next = messages[index + 1];
if (next?.role === 'assistant') continue;
messages.splice(index + 1, 0, {
role: 'assistant',
content: [{ type: 'text', text: '' }],
});
index += 1;
}
}
4 changes: 3 additions & 1 deletion electron/src/main/llm/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,9 @@ export async function* streamChat(params: StreamChatParams): AsyncGenerator<Stre
const historyMessages = toApiMessages(messages, thinkingReplay);

// System messages are handled by the `system` param in streamText.
const coreMessages = toModelMessages(historyMessages);
const coreMessages = toModelMessages(historyMessages, {
responsesReplay: thinkingReplay?.protocol === 'openai-responses',
});

// ── Filter and build tools ──
// Freeze session cwd from prompt context so tools match the turn's workspace.
Expand Down
1 change: 1 addition & 0 deletions electron/src/main/providers/catalog/trust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const TRUSTED_CATALOG_PROVIDER_POLICIES: readonly TrustedCatalogProviderP
{ id: 'google-gemini', authMethods: ['api-key', 'environment'], protocols: ['google-generative-ai'], allowsCustomModels: true },
{ id: 'xai', authMethods: ['api-key', 'environment'], protocols: ['xai'], allowsCustomModels: true },
{ id: 'opencode-go', authMethods: ['api-key', 'environment'], protocols: ['openai-compatible', 'openai-responses', 'anthropic-messages'], allowsCustomModels: true },
{ id: 'meta', authMethods: ['api-key', 'environment'], protocols: ['openai-responses'], allowsCustomModels: true, facets: ['thinking', 'cache'] },
{ id: 'lilac', authMethods: ['api-key', 'environment'], protocols: ['openai-compatible'], allowsCustomModels: true },
{ id: 'neuralwatt', authMethods: ['api-key', 'environment'], protocols: ['openai-compatible'], allowsCustomModels: true, facets: ['tiers'] },
{ id: 'generic-openai-compatible', authMethods: ['api-key', 'environment', 'none'], protocols: ['openai-compatible', 'openai-responses'], allowsCustomModels: true },
Expand Down
Loading
Loading