Both are added to the Z.AI static model list with their 1M-token context
and 128K max output. GLM-5.3-Flash is natively multimodal, so it gets the
vision and video capabilities — through an `override` rule, because the
provider's `defaults: { vision: false }` already set the flag and a fill
rule would have skipped it silently.
Neither model can stop thinking (`thinking.type` only accepts "enabled"),
so they get their own reasoning rule with low/high/max and no `disabled`,
placed before the `glm-5*` family rule that would otherwise swallow them.
This commit is contained in:
@@ -8,6 +8,14 @@ release PR may merge — and a section is closed at the commit that bumps it.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Z.AI's new models are selectable on the **Models** page: **GLM-5.3** and **GLM-5.3-Flash**,
|
||||||
|
both with a 1M-token context. GLM-5.3-Flash is natively multimodal, so images and videos
|
||||||
|
attached to a message are sent to it directly instead of as a file path. Both always think —
|
||||||
|
Z.AI does not allow turning it off — and the reasoning control offers *low / high / max*
|
||||||
|
(default *max*) instead of an on/off switch.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- The **Providers** page said *API key missing* on every provider, including the ones
|
- The **Providers** page said *API key missing* on every provider, including the ones
|
||||||
|
|||||||
@@ -10,6 +10,12 @@
|
|||||||
|
|
||||||
LLM client abstraction (OpenAI-compat, Anthropic, Ollama…). OpenAI-compatible provider *types* are runtime data, not code: `providers/declared.rs` loads `providers.yaml` at boot (see Config in [../CLAUDE.md](../CLAUDE.md)); only non-OpenAI-compatible or bespoke providers (anthropic, ollama, openai, openrouter) stay native. **Retriability** (`Model::is_retriable`, `agent-loop`) keys on the real HTTP status carried by `ModelError { status }`, **not** a substring of the message — a model id/token count containing "404"/"401" cannot mis-classify; 401/403/404/422 don't retry, 400/429/5xx/network do. **Request logging** is the `logging.rs::LoggingModel` decorator, attached by the *caller's* `ModelSelector` (`loop_adapters/selector.rs::SkaldSelector::with_log`) — never by `LlmManager`, which builds one shared client per model and cannot know whose traffic it serves. The decorator's `RequestLogTarget` carries the owner: metadata → `llm_requests` in the registry (`user_id`, the column the UI filters on), payload bodies/headers → `llm_request_payloads` in that user's own encrypted DB, keyed by `request_id`; session + frame come from the request's own `conversation`/`frame`, so kernel rounds, sub-agent frames and compaction summaries are all attributed with no extra plumbing (`ModelRequest::log` is unused here)
|
LLM client abstraction (OpenAI-compat, Anthropic, Ollama…). OpenAI-compatible provider *types* are runtime data, not code: `providers/declared.rs` loads `providers.yaml` at boot (see Config in [../CLAUDE.md](../CLAUDE.md)); only non-OpenAI-compatible or bespoke providers (anthropic, ollama, openai, openrouter) stay native. **Retriability** (`Model::is_retriable`, `agent-loop`) keys on the real HTTP status carried by `ModelError { status }`, **not** a substring of the message — a model id/token count containing "404"/"401" cannot mis-classify; 401/403/404/422 don't retry, 400/429/5xx/network do. **Request logging** is the `logging.rs::LoggingModel` decorator, attached by the *caller's* `ModelSelector` (`loop_adapters/selector.rs::SkaldSelector::with_log`) — never by `LlmManager`, which builds one shared client per model and cannot know whose traffic it serves. The decorator's `RequestLogTarget` carries the owner: metadata → `llm_requests` in the registry (`user_id`, the column the UI filters on), payload bodies/headers → `llm_request_payloads` in that user's own encrypted DB, keyed by `request_id`; session + frame come from the request's own `conversation`/`frame`, so kernel rounds, sub-agent frames and compaction summaries are all attributed with no extra plumbing (`ModelRequest::log` is unused here)
|
||||||
|
|
||||||
|
## `providers.yaml` — two traps in the model metadata
|
||||||
|
|
||||||
|
**`enrich` rules stop at the first glob that matches, and the default `mode: fill` skips a field that already has a value.** Both bite when adding a model to an existing family. Ordering: `glm-5.3-flash` matches `glm-5*` too, so a rule for it placed *after* the family rule never runs — the specific glob goes first, always. And `fill` means "the endpoint listing wins", which for a `static:` list is not the same as "nothing is set": `models.defaults` (today only `vision`) stamps every entry before `enrich` sees it, so a provider carrying `defaults: { vision: false }` — Z.AI does — needs **`mode: override`** to turn vision on for one model. A `fill` rule there parses, loads, logs nothing, and leaves the flag off; the only symptom is that images silently keep taking the textual `<system-extra>` path (see [Multimodal attachments](#multimodal-attachments)) on a model that can read them. `vision: true` also pushes the `vision` capability, but only when the rule actually applied.
|
||||||
|
|
||||||
|
**A `reasoning.modes` `values` list is the whole contract with the provider — it is not a superset to trim in the UI.** Whatever it lists is what can be sent, so a model that cannot stop thinking (`thinking.type` accepting only `"enabled"`: GLM-5.3 and GLM-5.3-Flash) simply omits `disabled` from its rule, rather than inheriting the family's `[disabled, enabled]` toggle and sending a value the API rejects. With `request: { kind: thinking }`, any value other than `disabled`/`enabled` is emitted as `{"thinking":{"type":"enabled"},"reasoning_effort":v}`, which is exactly the shape those models want.
|
||||||
|
|
||||||
## The provider API surface — the key is a boolean, never a value
|
## The provider API surface — the key is a boolean, never a value
|
||||||
|
|
||||||
**No provider endpoint ever returns a stored `api_key`, and the trap is that omitting it silently reads as "no key".** `LlmProviderInfo` (list) and `ProviderDetail` (`src/frontend/api/llm.rs`, the detail DTO — deliberately *not* `LlmProviderRecord`, which does carry the secret) both expose **`has_api_key: bool`** instead. That is the whole contract: the UI needs to know *whether* a key is on file, never what it is, and the browser is where a leaked key would end up in a devtools tab or a screenshot.
|
**No provider endpoint ever returns a stored `api_key`, and the trap is that omitting it silently reads as "no key".** `LlmProviderInfo` (list) and `ProviderDetail` (`src/frontend/api/llm.rs`, the detail DTO — deliberately *not* `LlmProviderRecord`, which does carry the secret) both expose **`has_api_key: bool`** instead. That is the whole contract: the UI needs to know *whether* a key is on file, never what it is, and the browser is where a leaked key would end up in a devtools tab or a screenshot.
|
||||||
|
|||||||
+13
-1
@@ -145,11 +145,17 @@ providers:
|
|||||||
- { key: api_key, label: "API Key", required: true, secret: true }
|
- { key: api_key, label: "API Key", required: true, secret: true }
|
||||||
models:
|
models:
|
||||||
# Z.AI exposes no GET /models endpoint; this mirrors the console menu.
|
# Z.AI exposes no GET /models endpoint; this mirrors the console menu.
|
||||||
static: [glm-5.2, glm-5.1, glm-5, glm-5-turbo, glm-4.7, glm-4.6, glm-4.5, glm-4-32b-0414-128k]
|
static: [glm-5.3, glm-5.3-flash, glm-5.2, glm-5.1, glm-5, glm-5-turbo, glm-4.7, glm-4.6, glm-4.5, glm-4-32b-0414-128k]
|
||||||
defaults: { vision: false }
|
defaults: { vision: false }
|
||||||
base_capabilities: [function_calling]
|
base_capabilities: [function_calling]
|
||||||
enrich:
|
enrich:
|
||||||
- { match: "*128k*", context_length: 131072 }
|
- { match: "*128k*", context_length: 131072 }
|
||||||
|
# GLM-5.3-Flash is the first natively multimodal GLM (image + video in).
|
||||||
|
# `mode: override` is load-bearing: `defaults.vision` already stamped
|
||||||
|
# every static model with `Some(false)`, and a fill rule skips a field
|
||||||
|
# that is already set — the flag would silently stay off.
|
||||||
|
- { match: "glm-5.3-flash*", mode: override, context_length: 1048576, max_completion_tokens: 131072, vision: true, add_capabilities: [video] }
|
||||||
|
- { match: "glm-5.3*", context_length: 1048576, max_completion_tokens: 131072 }
|
||||||
- { match: "glm-5*", context_length: 1048576 }
|
- { match: "glm-5*", context_length: 1048576 }
|
||||||
- { match: "glm-4.7*", context_length: 200000 }
|
- { match: "glm-4.7*", context_length: 200000 }
|
||||||
- { match: "glm-4.6*", context_length: 200000 }
|
- { match: "glm-4.6*", context_length: 200000 }
|
||||||
@@ -157,6 +163,12 @@ providers:
|
|||||||
reasoning:
|
reasoning:
|
||||||
request: { kind: thinking }
|
request: { kind: thinking }
|
||||||
modes:
|
modes:
|
||||||
|
# GLM-5.3 (and -Flash) cannot stop thinking: `thinking.type` only
|
||||||
|
# accepts "enabled", so the budget is picked with reasoning_effort
|
||||||
|
# alone and `disabled` is deliberately absent from the values.
|
||||||
|
- when: { models: ["glm-5.3*"] }
|
||||||
|
values: [low, high, max]
|
||||||
|
default: max
|
||||||
# GLM-5.2+ adds a graded effort on top of the thinking toggle.
|
# GLM-5.2+ adds a graded effort on top of the thinking toggle.
|
||||||
- when: { models: ["glm-5.2*"] }
|
- when: { models: ["glm-5.2*"] }
|
||||||
values: [disabled, minimal, low, medium, high, xhigh, max]
|
values: [disabled, minimal, low, medium, high, xhigh, max]
|
||||||
|
|||||||
Reference in New Issue
Block a user