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
1 change: 1 addition & 0 deletions packages/ai/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## [Unreleased]
- oMLX OpenAI-compatible completions now send `chat_template_kwargs.reasoning_effort` with `enable_thinking` when `thinkingFormat` is `qwen-chat-template`. Discovered oMLX models are treated as reasoning models with `low`/`medium`/`high` effort so local Qwen presets can differentiate roles without swapping weights.

## [0.14.0] - 2026-08-17
- Cursor native tool calls (shell/read/write/… oneof variants) now convert their protobuf payloads into plain JSON-safe data before attaching them as toolCall `arguments`: `$typeName` markers are stripped, safe-range bigints become numbers (decimal strings beyond `Number.MAX_SAFE_INTEGER`), byte arrays become base64 strings, and cycles/functions collapse to null. Raw protobuf-es payloads carry `bigint` fields (`fileSize`, `durationMs`, `fileOutputThresholdBytes`, …) that defeat `JSON.stringify`, which broke managed snapshot staging, JSONL transcript persistence, and provider replay — the issue #4578 local-snapshot producer defect class fixed at its producer boundary.
Expand Down
19 changes: 19 additions & 0 deletions packages/ai/src/model-thinking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ export function applyGeneratedModelPolicies(models: ApiModel<Api>[]): void {
if (source.id.split("/").at(-1)?.toLowerCase() === "muse-spark-1.2") {
source.reasoning = true;
}
if (source.provider === "omlx") {
source.reasoning = true;
}
const model = refreshModelThinking(source);
applyGeneratedModelPolicy(model);
models[index] = model;
Expand Down Expand Up @@ -430,6 +433,16 @@ function applyGeneratedModelPolicy(model: ApiModel<Api>): void {
};
delete model.compat.thinkingFormat;
}
if (model.provider === "omlx" && model.api === "openai-completions") {
model.compat = {
...(model.compat ?? {}),
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: true,
thinkingFormat: "qwen-chat-template",
reasoningContentField: "reasoning_content",
};
}
model.name = scrubGeneratedModelName(model.name);
if (
model.api === "openai-completions" &&
Expand Down Expand Up @@ -621,6 +634,9 @@ function inferDefaultEffort<TApi extends Api>(model: ApiModel<TApi>, parsedModel
) {
return GPT_5_5_DEFAULT_EFFORT;
}
if (model.provider === "omlx") {
return Effort.Medium;
}
return undefined;
}

Expand Down Expand Up @@ -784,6 +800,9 @@ function inferFallbackEfforts<TApi extends Api>(model: ApiModel<TApi>): readonly
return DEFAULT_REASONING_EFFORTS;
}
if (model.api === "openai-completions") {
if (model.provider === "omlx") {
return [Effort.Low, Effort.Medium, Effort.High];
}
const compat = resolveOpenAICompat(model as ApiModel<"openai-completions">);
if (compat.thinkingFormat === "openai" && compat.supportsReasoningEffort) {
return DEFAULT_REASONING_EFFORTS_WITH_XHIGH;
Expand Down
18 changes: 18 additions & 0 deletions packages/ai/src/provider-models/openai-compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,24 @@ export function omlxModelManagerOptions(config?: OmlxModelManagerConfig): ModelM
provider: "omlx",
baseUrl,
apiKey,
mapModel: (_entry, defaults) => ({
...defaults,
reasoning: true,
thinking: {
mode: "effort",
minLevel: Effort.Low,
maxLevel: Effort.High,
defaultLevel: Effort.Medium,
levels: [Effort.Low, Effort.Medium, Effort.High],
},
compat: {
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: true,
thinkingFormat: "qwen-chat-template",
reasoningContentField: "reasoning_content",
},
}),
}),
};
}
Expand Down
6 changes: 5 additions & 1 deletion packages/ai/src/providers/openai-completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1398,8 +1398,12 @@ function buildParams(
// Qwen uses top-level enable_thinking: boolean
params.enable_thinking = !!options?.reasoning && !options?.disableReasoning;
} else if (supportsReasoningParams && compat.thinkingFormat === "qwen-chat-template" && model.reasoning) {
const enableThinking = !!options?.reasoning && !options?.disableReasoning;
params.chat_template_kwargs = {
enable_thinking: !!options?.reasoning && !options?.disableReasoning,
enable_thinking: enableThinking,
...(enableThinking && options?.reasoning
? { reasoning_effort: mapReasoningEffort(options.reasoning, compat.reasoningEffortMap) }
: {}),
};
} else if (supportsReasoningParams && compat.thinkingFormat === "openrouter" && model.reasoning) {
// OpenRouter normalizes reasoning across providers via a nested reasoning object.
Expand Down
30 changes: 30 additions & 0 deletions packages/ai/test/openai-completions-compat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ function getNestedBoolean(value: unknown, key: string): boolean | undefined {
const property = Reflect.get(obj, key);
return typeof property === "boolean" ? property : undefined;
}
function getNestedString(value: unknown, key: string): string | undefined {
const obj = toObject(value);
if (!obj) return undefined;
const property = Reflect.get(obj, key);
return typeof property === "string" ? property : undefined;
}

function createSseResponse(events: unknown[]): Response {
const payload = `${events.map(event => `data: ${typeof event === "string" ? event : JSON.stringify(event)}`).join("\n\n")}\n\n`;
Expand Down Expand Up @@ -430,6 +436,30 @@ describe("openai-completions compatibility", () => {
const chatTemplateArgs = getNestedObject(payload, "chat_template_kwargs");
expect(getNestedBoolean(chatTemplateArgs, "enable_thinking")).toBe(true);
});
it("maps oMLX reasoning effort into chat_template_kwargs.reasoning_effort", async () => {
const model: Model<"openai-completions"> = {
...getBundledModel("openai", "gpt-4o-mini"),
api: "openai-completions",
provider: "omlx",
id: "Qwen3.6-35B-A3B-8bit",
reasoning: true,
compat: {
thinkingFormat: "qwen-chat-template",
supportsReasoningEffort: true,
},
};
const { promise, resolve } = Promise.withResolvers<unknown>();
streamOpenAICompletions(model, baseContext(), {
apiKey: "test-key",
reasoning: "high",
signal: createAbortedSignal(),
onPayload: payload => resolve(payload),
});
const payload = await promise;
const chatTemplateArgs = getNestedObject(payload, "chat_template_kwargs");
expect(getNestedBoolean(chatTemplateArgs, "enable_thinking")).toBe(true);
expect(getNestedString(chatTemplateArgs, "reasoning_effort")).toBe("high");
});

it("treats finish_reason end as stop", async () => {
const model: Model<"openai-completions"> = {
Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## [Unreleased]
- Discovered oMLX models now keep thinking metadata (`reasoning: true`, `supportsReasoningEffort`, `thinkingFormat: qwen-chat-template`) so `macos-omlx-*` role suffixes (`:low`/`:medium`/`:high`) survive clamp and reach oMLX as `chat_template_kwargs.reasoning_effort`.
- Added built-in `MACOS LOCAL (OMLX)` model profiles (`macos-omlx-fast`, `macos-omlx-balanced`, `macos-omlx-quality`, `macos-omlx-abliterated-fast`, `macos-omlx-abliterated-balanced`) for oMLX local inference on Apple Silicon Macs with native full context support and single-LLM thinking effort role mappings to eliminate model swap latency.
- Fixed an HTTP 400 that killed every deep-interview session on the `google-antigravity` provider before the first assistant turn. The Round-0 topology `ask` schema pinned `round` with `z.literal(0)`, which zod serializes as `const: 0` and the Cloud Code Assist normalizer rewrites to a numeric `enum: [0]` — a shape CCA rejects (`TYPE_STRING`). `round` is now pinned with an integer range `[0, 0]` instead, so the wire schema carries `type: integer` with the bounds spilled into the description (the same treatment `ambiguity` already gets) and no numeric enum remains. Runtime contract unchanged: only `0` validates (#4606).
- The terminal-app integration docs now cite the upstream work that backs each support rating: Gajae Code is proposed for Paseo's in-app ACP provider catalog ([getpaseo/paseo#3471](https://github.com/getpaseo/paseo/pull/3471)) and for Orca's built-in agent registry ([stablyai/orca#15025](https://github.com/stablyai/orca/pull/15025)), while T3 Code has no GJC harness and the integration shape is under discussion upstream ([pingdotgg/t3code#7290](https://github.com/pingdotgg/t3code/discussions/7290)).
Expand Down
22 changes: 20 additions & 2 deletions packages/coding-agent/src/config/model-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type Context,
codexContextOverrideKey,
createModelManager,
Effort,
enrichModelThinking,
getBundledModels,
getBundledProviders,
Expand Down Expand Up @@ -3042,7 +3043,7 @@ export class ModelRegistry {
api,
provider: providerConfig.provider,
baseUrl: requestBaseUrl,
reasoning: referenceModel?.reasoning ?? false,
reasoning: providerConfig.provider === "omlx" ? true : (referenceModel?.reasoning ?? false),
thinking: referenceModel?.thinking,
input: referenceModel?.input ?? ["text"],
output: referenceModel?.output,
Expand All @@ -3069,8 +3070,25 @@ export class ModelRegistry {
...referenceModel?.compat,
supportsStore: false,
supportsDeveloperRole: false,
supportsReasoningEffort: false,
supportsReasoningEffort: providerConfig.provider === "omlx",
...(providerConfig.provider === "omlx"
? {
thinkingFormat: "qwen-chat-template" as const,
reasoningContentField: "reasoning_content" as const,
}
: {}),
},
...(providerConfig.provider === "omlx"
? {
reasoning: true,
thinking: {
mode: "effort" as const,
minLevel: Effort.Low,
maxLevel: Effort.High,
defaultLevel: Effort.Medium,
},
}
: {}),
}),
);
}
Expand Down
9 changes: 8 additions & 1 deletion packages/coding-agent/test/model-profiles-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,13 @@ const commandCodeGoatModels = new Set([
"deepseek/deepseek-v4-pro",
"moonshotai/Kimi-K3",
]);
const macosOmlxModels = new Set([
"Qwen3.6-35B-A3B-4bit",
"Qwen3.6-35B-A3B-8bit",
"Qwen3.6-35B-A3B-bf16",
"Qwen3.8-27B-Abliterated-MLX-4bit",
"Qwen3.8-27B-Abliterated-MLX-6bit",
]);

function selectorExists(selector: string): boolean {
const selectorWithoutThinking = splitSelectorThinkingSuffix(selector).selector;
Expand All @@ -698,7 +705,7 @@ function selectorExists(selector: string): boolean {
if (!parsed) return false;
if (parsed.provider === "grok-build") return ["grok-composer-2.5-fast", "grok-build"].includes(parsed.id);
if (parsed.provider === "commandcode-goat") return commandCodeGoatModels.has(parsed.id);
if (parsed.provider === "omlx") return true;
if (parsed.provider === "omlx") return macosOmlxModels.has(parsed.id);
return (modelsJson as Record<string, Record<string, unknown>>)[parsed.provider]?.[parsed.id] !== undefined;
}

Expand Down
27 changes: 27 additions & 0 deletions packages/coding-agent/test/omlx-discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,31 @@ describe("ModelRegistry oMLX Discovery", () => {
expect(registry.find("omlx", "valid-model")?.contextWindow).toBe(131072);
expect(registry.find("omlx", "valid-model")?.maxTokens).toBe(8192);
});
test("marks discovered oMLX models as reasoning with low/medium/high effort", async () => {
using _hook = hookFetch(input => {
if (!String(input).includes(":8080/v1/models")) return new Response(null, { status: 404 });
return new Response(
JSON.stringify({
data: [{ id: "Qwen3.6-35B-A3B-8bit", max_model_len: 262144 }],
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
});

const registry = new ModelRegistry(authStorage, modelsJsonPath);
await registry.refresh();

const model = registry.find("omlx", "Qwen3.6-35B-A3B-8bit");
expect(model?.reasoning).toBe(true);
expect(model?.thinking).toMatchObject({
mode: "effort",
minLevel: "low",
maxLevel: "high",
defaultLevel: "medium",
});
expect(model?.compat).toMatchObject({
supportsReasoningEffort: true,
thinkingFormat: "qwen-chat-template",
});
});
});
Loading