Skip to content

feat(core,runtime-host,desktop): declare per-model relay profiles for OpenAI-compatible connections 添加了自定义 openai 时对推理和 vision 能力和上下文长度自定义 - #2391

Closed
sanshanya wants to merge 1 commit into
apache:mainfrom
sanshanya:feat/openai-compatible-thinking-controls

Conversation

@sanshanya

@sanshanya sanshanya commented Aug 7, 2026

Copy link
Copy Markdown
English

Summary

OpenAI-compatible relays can front models that Maka's built-in metadata does not know about. For those models, Maka cannot reliably infer three user-visible capabilities:

  • which reasoning-effort levels the relay/model accepts
  • whether image input should be treated as supported
  • the model's context-window size

This PR adds first-class, per-model relay declarations for those capabilities and carries them through Desktop, embedded storage, Runtime Host, Runtime, CLI, and Headless execution paths.

Refs #2219.

Design

Relay declarations live on the connection as a separate typed field:

relayModelProfiles: Record<
  modelId,
  {
    thinkingLevels?: ThinkingLevel[];
    vision?: boolean;
    contextWindow?: number;
  }
>;

They intentionally do not live inside fetched models[] rows.

models[] represents provider/discovery data and may be replaced by /models refreshes. Relay declarations are user-owned configuration with a different lifecycle. Keeping the two authorities separate avoids re-merging user state after every discovery refresh and gives endpoint changes explicit semantics.

If baseUrl changes without a replacement profile table, declarations associated with the previous endpoint are retired.

Storage boundaries enforce two invariants:

  1. relay profiles are accepted only for openai-compatible connections
  2. profile keys remain a subset of enabledModelIds, so disabling or retiring a model also removes its declaration

Runtime reads go through the provider-gated relayModelProfile(connection, modelId) seam. Built-in providers therefore continue to use the existing metadata authority.

Runtime Host

Embedded and Runtime Host modes use the same typed profile model and lifecycle semantics.

Runtime Host does not place the entire profile table into a single catalog header item. Instead, an optional relayProfile travels with each paginated enabled_model_id item, and the client reconstructs the table.

This reuses the existing catalog paginator and avoids introducing a separate unsplittable payload limit.

Connection updates use whole-table tri-state semantics:

  • omitted: keep the existing table unchanged
  • null: clear the table
  • table: replace the table

Config import adapts snapshot semantics to this update contract. Model refreshes, enabled-model changes, and endpoint changes preserve the same profile invariants in both storage implementations.

Thinking wire

Generic relay declarations support exact reasoning-effort levels from minimal through max.

They intentionally do not declare off, because off is a provider-specific disable-wire contract rather than a normal reasoning-intensity tier.

For custom relay connections, the raw connection slug remains the provider identity, while provider options use the OpenAI-compatible SDK's canonical camelCase alias.

A fake-fetch regression test executes the actual model path and verifies that a declared level reaches the final HTTP request body as:

{
  "reasoning_effort": "high"
}

The same test verifies that this path does not emit the SDK's deprecated raw provider-options-key warning.

Settings

For every enabled model on an openai-compatible connection, Settings exposes:

  • Thinking effort — exact supported effort levels
  • Vision input — Auto / Enabled / Disabled
  • Context window — optional positive-integer override

Edits stay in a connection-scoped local draft and are committed with one explicit save action.

Switching connections always reseeds the draft, preventing unsaved declarations from one connection from being displayed or saved into another.

Validation

Regression coverage added by this PR includes:

  • relay-profile normalization and provider gating
  • strict catalog decoding and malformed-input handling
  • embedded and Runtime Host create/update/import semantics
  • profile pruning after model disablement, model refresh, and endpoint changes
  • paginated Runtime Host catalog transport and client reconstruction
  • session thinking-level validation and runtime execution authority
  • actual HTTP request-body verification for reasoning_effort
  • vision and context-window resolution through relay declarations
  • connection-scoped Settings draft lifecycle and malformed local-state sanitization
  • CLI and Headless connection-aware capability resolution

GitHub Actions for this fork PR are currently awaiting maintainer approval before jobs can run.

Review focus

  • Is separating fetched models[] data from user-owned relayModelProfiles the right ownership boundary?
  • Do embedded storage and Runtime Host enforce equivalent profile lifecycle semantics?
  • Is carrying relayProfile on paginated enabled_model_id items the smallest coherent Runtime Host protocol extension?
  • Are the generic-relay thinking semantics conservative enough, especially the deliberate exclusion of off?
简体中文

概要

OpenAI-compatible 中转站可能代理 Maka 内置模型元数据完全不了解的模型。对于这些模型,Maka 无法可靠推断三个直接影响用户使用的能力:

  • 中转站 / 模型实际接受哪些 reasoning effort 档位
  • 是否应当视为支持图片输入
  • 模型的上下文窗口长度

本 PR 为这三项能力增加一等的、按模型声明的 relay profile,并把同一份声明贯通 Desktop、embedded storage、Runtime Host、Runtime、CLI 和 Headless 执行路径。

Refs #2219

设计

Relay 声明作为连接上的独立 typed field 存储:

relayModelProfiles: Record<
  modelId,
  {
    thinkingLevels?: ThinkingLevel[];
    vision?: boolean;
    contextWindow?: number;
  }
>;

这些声明刻意不放进 fetched models[] 条目。

models[] 属于 provider / discovery 数据,可能被 /models 刷新整体替换;relay declaration 则属于用户配置,两者生命周期不同。

把两种 authority 分开后,不需要在每次模型刷新时把用户状态重新 merge 回模型条目,同时 endpoint 变化也有明确语义:

如果 baseUrl 发生变化,而同一次更新没有提交新的 profile table,则旧 endpoint 对应的声明会被清除。

Storage boundary 强制两个不变量:

  1. 只有 openai-compatible connection 可以持有 relay profile
  2. profile key 必须始终属于 enabledModelIds;禁用或淘汰模型时同步移除对应声明

Runtime 中的读取统一经过 provider-gated relayModelProfile(connection, modelId) seam,因此内置 provider 仍然完全以已有 metadata chain 为准。

Runtime Host

Embedded 和 Runtime Host 两种模式使用同一套 typed profile 模型和生命周期语义。

Runtime Host 不会把整张 profile table 放进单个 catalog header item。

可选的 relayProfile 会跟随已经分页的 enabled_model_id item 传输,再由 client 重建 profile table。

这样可以直接复用已有 catalog paginator,同时避免额外引入一张不可拆分的大 payload 及其独立 size limit。

Connection update 对整张 table 使用三态语义:

  • omitted:保持原 table 不变
  • null:清空 table
  • table:整体替换

Config import 会把 snapshot semantics 正确适配到这一更新协议。模型刷新、enabled model 变化和 endpoint 变化,在 embedded 与 Runtime Host 两种存储实现中遵守同样的 profile invariant。

Thinking wire

Generic relay 可以精确声明从 minimalmax 的 reasoning-effort levels。

不会允许 generic relay 声明 off,因为 off 不是普通的 reasoning intensity,而是 provider-specific 的“关闭推理”wire contract。

对于自定义 relay connection,原始 connection slug 仍然作为 provider identity;provider options 则使用 OpenAI-compatible SDK 的 canonical camelCase alias。

fake-fetch 回归测试会真正执行实际 model path,并验证声明的档位最终进入 HTTP request body:

{
  "reasoning_effort": "high"
}

同一个测试还会确认该路径不会产生 SDK 对 raw provider-options key 的 deprecated warning。

设置界面

每个 openai-compatible connection 的 enabled model 都可以声明:

  • Thinking effort — 精确选择实际支持的 effort levels
  • Vision input — Auto / Enabled / Disabled
  • Context window — 可选的正整数覆盖值

所有修改首先保存在 connection-scoped local draft 中,通过一次显式保存提交。

切换 connection 时 draft 会强制重新初始化,避免 A 连接未保存的声明显示在 B 连接中,或被错误保存到 B。

验证

本 PR 新增的回归覆盖包括:

  • relay profile normalization 与 provider gating
  • strict catalog decoding 与 malformed input 处理
  • embedded / Runtime Host 的 create、update、import 语义
  • 禁用模型、模型刷新和 endpoint 变化时的 profile pruning
  • Runtime Host 分页 catalog transport 与 client reconstruction
  • session thinking-level validation 与 runtime execution authority
  • reasoning_effort 的真实 HTTP request-body 验证
  • relay declaration 对 vision / context-window resolution 的影响
  • Settings draft 的 connection 生命周期与 malformed local state sanitize
  • CLI / Headless 的 connection-aware capability resolution

当前这个 fork PR 的 GitHub Actions 仍在等待 maintainer approval,jobs 尚未实际运行。

Review focus

  • fetched models[] 与用户拥有的 relayModelProfiles 分离,是否是合适的数据 ownership boundary?
  • embedded storage 与 Runtime Host 是否保持了等价的 profile lifecycle semantics?
  • 在分页 enabled_model_id item 上携带 relayProfile,是否是最小且合理的 Runtime Host protocol extension?
  • generic relay 的 thinking semantics 是否足够保守,尤其是刻意不允许声明 off

Screenshots / 截图

image image image

@sanshanya sanshanya changed the title feat(core,runtime-host,desktop): declare relay model profiles per model 添加了自定义 openai 时对推理强度和 vision 相关能力的支持和上下文长度自定义 feat(core,runtime-host,desktop): declare per-model relay profiles for OpenAI-compatible connections 添加了自定义 openai 时对推理和 vision 能力和上下文长度自定义 Aug 7, 2026
@sanshanya
sanshanya force-pushed the feat/openai-compatible-thinking-controls branch from 44c16e5 to 49f9c8f Compare August 7, 2026 05:37
A generic openai-compatible relay fronts models the built-in metadata chain
has never heard of: no thinking-level menu, no vision decision, a guessed
context window. This adds first-class, per-model user declarations for the
three facts only the user can decide.

Data model: `Connection.relayModelProfiles: Record<modelId,
{ thinkingLevels?; vision?; contextWindow? }>` on the single connection type
chain. Two invariants are enforced at every store boundary — profiles exist
only on openai-compatible connections (the metadata chain is the truth
elsewhere), and only for ids in `enabledModelIds` (both stores prune the
table when a model is disabled, so no declaration can outlive its row).
Every read enters through one gated seam, `relayModelProfile(connection,
modelId)`; writes are sanitized by `normalizeRelayModelProfiles`, and the
catalog codec validates strictly (closed level vocabulary, subset of the
selection, positive-integer context window) — a malformed legacy document is
noise, a malformed catalog document is a loud failure.

Updates are whole-table tri-state: an absent key leaves the stored table
untouched (capability-blind writers cannot clobber), `null` clears it, a
table replaces it. A baseUrl change in the same update retires an untouched
table unless the update submits one of its own — declarations belong to the
endpoint they were declared against. The embedded store mirrors the same
merge rules, and config import round-trips the typed field instead of an
extras bag.

Host wire: profiles travel on their own `enabled_model_id` page item
(`relayProfile`), never in one header table — a header item is atomic to the
paginator's 48 KiB page budget, so a long declaration list there would have
made the whole connection unreadable. The client reader reassembles the
per-model map; compatibility epoch bumps to 6. Session-model gating and
execution authority consume the same typed table, so a level that passes the
gate is exactly what the wire can send; the connection slug is the
provider-options namespace, and a fake-fetch test pins the translated
`reasoning_effort` request body.

Settings UI: each enabled model row edits a local draft (precise
multi-select of thinking levels — generic relays never declare the 'off'
wire —, Auto/Enabled/Disabled vision tri-state, positive-integer context
window) committed by one explicit 保存能力声明; there is no per-keystroke
persistence machinery. The add-provider form declares the connection's
first (default) model at create time through the same typed field.
@sanshanya
sanshanya force-pushed the feat/openai-compatible-thinking-controls branch from 49f9c8f to dff3627 Compare August 7, 2026 11:24
@jackwener

Copy link
Copy Markdown
Member

Review decision

Reviewed against first principles and current main.

Problem is real: openai-compatible relays front unknown models; Maka cannot infer thinking levels / vision / context window from built-in metadata alone.

Design is correct: separating user-owned relayModelProfiles from fetched models[] is the right ownership boundary (different lifetimes). Provider-gating to openai-compatible, pruning keys to enabledModelIds, and endpoint-change invalidation are coherent. Wire path verified (including request-body reasoning_effort). Prefer this over #2351's models[].thinkingOptions approach (closed as superseded).

Issues fixed while landing:

  1. Rebase conflict in provider-add-form.tsx (kept opencode-free defaults + createdDefaultModel).
  2. Bug: create() pruned profiles against only defaultModel, dropping non-default profiles on multi-model create/import. Fixed + regression test.

Landing as maintainer branch PR (rebased + fix) so it can merge cleanly without write access to the fork head.

@jackwener

Copy link
Copy Markdown
Member

Landing via rebased+fixed #2463 (see review comment). Thank you @sanshanya!

@jackwener jackwener closed this Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants