| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
feat(extraction): media (audio/video) extraction + AliDocMind provider (#887) * feat(extraction): add AliDocMind provider + media extraction abstraction Adds AliDocMind (Aliyun Document Mind LLM version) as a new vendor alongside unpdf/MinerU, and introduces the media (audio/video) extraction layer that mirrors the document extraction one. Document side (file: pdf/docx/pptx/xlsx/images): - PDF_PROVIDERS gains an `alidocmind` entry; parseWithAliDocMind() maps layouts[] -> ParsedPdfContent. Flows through the existing document extractor registry, so AliDocMind is selectable anywhere MinerU is. - PDFParserConfig / DocumentExtractorConfig gain accessKeyId/accessKeySecret (AliDocMind uses AK/SK, not a single apiKey); env fallback via ALIDOCMIND_ACCESS_KEY_ID / ALIDOCMIND_ACCESS_KEY_SECRET. Media side (mp4/mp3/wav/... -> MediaArtifact): - New lib/media-parse/ domain mirroring lib/pdf/ (types/constants/providers). parseMedia() maps AliDocMind segments[]/audio_frames/video_frames -> MediaArtifact (transcript + keyframes). - MediaExtractorProvider interface + media registry + extractMedia() entry, symmetric to DocumentExtractorProvider / extractDocument(). - MediaArtifact and the ExtractionResult/Artifact/Error/Job envelope live in lib/document/types.ts (re-exported from @/lib/document). Shared AliDocMind SDK wrapper (lib/pdf/alidocmind-client.ts) handles the submit -> poll -> get flow for both sides via @alicloud/docmind-api20220711. Tests: env-gated smoke test (tests/document/alidocmind.smoke.test.ts) drives a real PDF and a real video through AliDocMind; media-artifact type test; extractor-registry test updated for the new provider. Part of #621 (MAIC ETL). Media extraction is the sibling to the document extraction landed in #741. * feat(extraction): wire AliDocMind AK/SK through settings UI + routes Surfaces AliDocMind in the (post-#837) Document Parsing settings panel as a peer of unpdf/MinerU — one panel, one credential entry, more supported formats. Threads Aliyun AccessKey ID/Secret from the store through the extraction routes. - Store: pdfProvidersConfig + setPDFProviderConfig gain accessKeyId/accessKeySecret; default alidocmind entry added. - pdf-settings.tsx: AliDocMind branch renders AccessKey ID + Secret inputs (secret masked with show/hide) and a Test Connection button; request-URL preview shows the DocMind endpoint. Provider auto-appears in the panel via PDF_PROVIDERS; supported-format badges come from #837's registry (ALIDOCMIND_MIMES added to lib/document/mime.ts). - verify-pdf-provider route: alidocmind branch verifies AK/SK via a lightweight authenticated probe (verifyAliDocMindCredentials) — auth-level errors fail, anything else passes. - extract-document route + generation flow (app/page.tsx, generation-preview): accessKeyId/accessKeySecret carried through session → FormData → config. - i18n: alidocmindAccessKeyId / alidocmindAccessKeySecret in 8 locales. - Icon: reuse /logos/bailian.svg (Aliyun family) instead of a missing asset. Verified end-to-end against the running app: AliDocMind panel renders, Test Connection returns "连接成功" through the real Aliyun API. Part of #886. * feat(extraction): route audio/video uploads through extractMedia() (reuse document path) Media uploads now flow through the same upload picker, /api/extract-document route, and generation pipeline as documents — no separate upload area. Only the extraction differs: media mimes dispatch to extractMedia() -> MediaArtifact, which is flattened to the text shape the generation pipeline already consumes. - mime.ts: register audio/video formats in DOCUMENT_FORMATS (accept string, extension map, badges resolve for them); add MEDIA_PROVIDER_SUPPORTED_MIME_TYPES + SUPPORTED_MEDIA_MIME_TYPES, kept separate from PROVIDER_SUPPORTED_MIME_TYPES so the document drift-guard stays document-only. mimesForProviders() folds in a provider's media mimes, so the existing upload helpers (getAcceptStringForProviders / isMimeSupportedByProviders / format badges) cover media automatically when the provider supports it. - extract-document route: media mimes dispatch to extractMedia(); MediaArtifact flattened to timestamped text (synopsis + transcript + keyframes). - Tests: 6 media cases in mime.test.ts (accept/validation/badges/normalization). Verified: uploading a real video through the route returns synopsis + timestamped transcript/keyframes as text (curl, real AliDocMind key). Part of #886. * feat(extraction): use Alibaba Cloud icon for AliDocMind provider Replace the placeholder bailian.svg with a dedicated Alibaba Cloud icon mark (the square symbol from the official wordmark, text removed) so the provider reads as Aliyun rather than Bailian. Square aspect matches the other provider icons. Source: Alibaba Cloud / Alibaba Group brand assets. * style: prettier format AliDocMind + media extraction files * fix(extraction): harden AliDocMind — SSRF guard, cred verify, env gate, table text Addresses review findings on the AliDocMind provider: - SSRF (high): the media branch of /api/extract-document now runs the same validateUrlForSSRF check on a client-supplied baseUrl as the document branch, so an audio/video upload can't point the server's Aliyun SDK at an internal host. - Credential verify (high): verifyAliDocMindCredentials now whitelists success signals instead of blacklisting auth errors. Probed against the real API: valid creds + bogus job returns a no-throw "BizIdNotExistOrResultExpired" body; invalid creds throw InvalidAccessKeyId.NotFound. Only a no-throw response or a "biz-not-found" business error counts as valid — an unreachable endpoint, a localized error, or throttling now correctly reports failure instead of a false "connection successful". - Env-fallback gate (med): resolveCredentials no longer reads ALIDOCMIND_ACCESS_KEY_ID/SECRET unconditionally. Env fallback is opt-in via allowEnvFallback, which the route sets only for a server-managed provider, so an unauthenticated client request can't silently run on the server account. - getDocParserResult error body (med): fetchResult throws on a non-200 result envelope instead of returning {} (empty text presented as success). - Media pagination (med): stop after the first page when segments[] are present — layoutNum/layoutStepSize address layout blocks, not media segments, so re-requesting would loop over the same segments to the safety cap. - Table content (med): tables/charts carry content in llmResult, not markdownContent; the layouts→text mapping now prefers llmResult for those types so table content isn't dropped (tables: true was advertised). - Dead code: remove getCurrentMediaParseConfig (referenced non-existent store fields; the single-panel UI reuses pdfProvidersConfig). - Dedup: media MIME list lives only in lib/document/mime.ts (ALIDOCMIND_MEDIA_MIMES); the media registry imports it. - Tests: smoke test asserts durationMs is in ms (>1000 for a ~52s clip) to guard a ms/s unit mismatch; uses allowEnvFallback for the env-cred path. Verified with the real key: PDF + video smoke tests pass; verify classifies valid vs invalid creds correctly. Part of #886. * feat(extraction): extract AliDocMind images to base64 (parity with unpdf/MinerU) AliDocMind embeds figure/picture image URLs inside each layout's markdownContent (markdown ``), not a dedicated field, and the URLs are short-lived OSS signed links. Previously we emitted `images: []` and left the expiring URLs inside the extracted text. Now, for figure/picture layouts we: - parse the OSS image URL out of markdownContent, - fetch it at extraction time (before the signature expires) and re-encode to PNG base64 via sharp — the same base64 `images[]` contract unpdf/MinerU produce, so downstream storeImages → IndexedDB → slide works unchanged, - populate metadata.pdfImages + imageMapping (the generation flow prefers pdfImages), - strip the remote-URL markdown from the emitted text so expiring links don't leak into the prompt. Downloads run concurrently; a failed/`sharp` image is dropped, never failing the whole parse. Also fixes the prior over-broad table handling: only `table` blocks read llmResult; `figure` is treated as an image (chart-figure llmResult still kept in text). Verified with the real key: a sample PDF yields 24 base64 images in both images[] and metadata.pdfImages, and no oss-cn-hangzhou URLs remain in text. Part of #886. * docs(test): note AliDocMind video smoke test is non-deterministic server-side * fix(extraction): correct AliDocMind pageCount (pageNum is 0-based) Verified against a real response: AliDocMind reports pageNum 0..13 and pageCountEstimate 13 for a 14-page document — both are 0-based. The metadata pageCount previously used pageCountEstimate directly, undercounting by one. Use the already-1-based maxPage, falling back to pageCountEstimate+1 only when no blocks were seen. Document the 0-based convention at the normalization site. * fix(extraction): address AliDocMind review — verify/SSRF/config/media (P1+P2) Resolves all P1/P2 findings from the cross-review on #887. P1 (blocking): 1. verifyAliDocMindCredentials now inspects the no-throw response body.code. An OSS-only key returns NoPermission without throwing; previously that was green-lit as "connection successful" then failed at extraction. Only a success/200 or the job-not-found probe code is accepted. Deterministic mocked test added (tests/document/alidocmind-verify.test.ts). 2. verify route trust boundary: managed → server-owned AK/SK + default endpoint only (ignore client values); unmanaged → client creds only, never env fallback, and the client endpoint is SSRF-validated before signing. 3. image fetch hardened: restricted to Aliyun OSS hosts, redirects disallowed, per-image byte cap, image-count cap, bounded concurrency (was unbounded Promise.all over provider-returned URLs). 4. AliDocMind is now selectable in the generation toolbar — availability recognizes the AK/SK pair, not just apiKey. P2 (correctness): 5. Explicit server-config for the AK/SK pair (applyAliDocMindFallback + resolveManagedAliDocMindCredentials); verify and extract now resolve managed/env identically instead of verify-uses-env / extract-rejects. 6. Poll loop checks body.code before status, so a body-level error (e.g. NoPermission) fails fast instead of retrying for the full 15 min. 7. Image page numbers preserved through fetch/filter — no longer hard-coded to page 1, so multi-page image→page association is correct. 8. Empty media extraction (no synopsis/transcript/keyframes) returns 422 PARSE_FAILED instead of HTTP 200 with empty text. 9. Format matrix trimmed to the official contract: images JPG/JPEG/PNG/BMP/GIF (dropped WebP/JP2), media MP4/MKV/AVI/MOV/WMV/MP3/WAV/AAC (dropped M4A). 10. A document-only provider (unpdf/mineru) uploaded with a media file now returns a clear 4xx instead of an opaque 500. P3: credential-verify failures return INVALID_CREDENTIALS 4xx (not INTERNAL_ERROR 500); formatTimestamp emits HH:MM:SS past one hour. Verified with the real key: verify classifies valid→ok and invalid→fail; PDF (24 images, correct page numbers) and MP4 extraction pass end-to-end. * fix(extraction): make AliDocMind selectable + correct analysis label for media Two UI/UX fixes found while manually testing the AliDocMind flow end-to-end: - Persisted-state backfill: add ensureBuiltInPDFProviders so a PDF/document provider added after a user's settings were persisted (AliDocMind) is backfilled into pdfProvidersConfig on rehydrate. Without it the provider never appeared in the store, so it couldn't be selected and never picked up its server-configured flag. Wired into both persist migrate() and merge(), mirroring the existing image/video/web-search backfills. - Analysis step label: getGenerationStepText showed "解析 document 文件" for audio/video (the type map fell through to the literal "document"). Documents keep their precise token (PDF/DOCX/PPTX/XLSX/images); audio/video now use a dedicated, locale-correct string (generation.analyzingMediaMaterial, added to all 8 locales) instead of forcing a format token into the "{{type}} 文件" template. Manually verified end-to-end (real key): PDF, MP4, and MP3 (audio extracted from the sample video) each extract and drive full course generation; a DocMind-restricted AK/SK (OSS-only) now correctly fails verification with NoPermission (400) instead of a false "connection successful". * fix(extraction): address 2nd-round AliDocMind review (managed creds, stream cap, empty verify) Resolves the three follow-up findings on #887: 1. [P1] YAML-managed AliDocMind creds now reach extraction. Both extract paths (document + media) previously cleared the managed AK/SK and relied on an env-only fallback, so a YAML-only deployment verified but failed to extract. They now resolve server-owned creds via the shared resolveManagedAliDocMindCredentials() (env OR YAML), matching the verifier. Regression test added for YAML-only creds with no ALIDOCMIND_* env. 2. [P1] Image download is now size-capped while streaming. Instead of buffering the whole response and checking length afterward, the body is read chunk by chunk with a cumulative byte count that aborts the moment it exceeds the cap — a missing/false Content-Length can no longer exhaust memory. Host allowlist tightened to oss-*.aliyuncs.com. Tests: non-OSS host refusal, no-Content-Length overflow abort, declared-oversize rejection. 3. [P2] Empty verification body no longer counts as success. Removed the codeStr === '' branch from the positive-signal whitelist (a working key always returns the job-not-found business code for the bogus probe id). Regression test added for empty and absent bodies. Rebased onto main (package.json: kept @openmaic/storage + AliCloud deps). Verified with the real key: PDF + MP4 extraction still pass end-to-end. * fix(extraction): merge AliDocMind AK/SK into a baseUrl-configured YAML entry Follow-up to the YAML-managed credential fix. When a YAML `pdf.alidocmind` entry also specifies `baseUrl`, the generic loadEnvSection() (pdf requires a baseUrl) creates the pdf.alidocmind entry copying only apiKey/baseUrl/models/ proxy — never AK/SK. applyAliDocMindFallback() then returned early because the entry already existed, so the provider was "managed" but had no usable credentials, and resolveManagedAliDocMindCredentials() returned undefined (verify + extract both silently lost the creds). Merge the AK/SK into the existing entry instead of returning early. Added a regression test with baseUrl + accessKeyId + accessKeySecret together (the previous test omitted baseUrl, so the generic loader skipped the entry and the bug was masked). * fix(extraction): don't mark AliDocMind managed without AK/SK; align poll code check Two edge cases from a follow-up adversarial pass: - [MED] A YAML `pdf.alidocmind` entry with `baseUrl` but no AK/SK (and no env AK/SK) made the generic loader create the entry → isServerConfigured=true (managed) → but resolveManagedAliDocMindCredentials() returned undefined, so the provider was locked out AND client-entered AK/SK were silently dropped. applyAliDocMindFallback now deletes a credential-less entry so the provider stays UNMANAGED (clients supply their own creds). Regression test added. - [LOW] verify accepted a `code: "success"` body but the extraction poll loop threw on any non-"200" code — a success-shaped status would pass verification then fail extraction. The poll loop now treats "200"/"success" as benign, matching verifyAliDocMindCredentials. --------- Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn> | 3 个月前 | |
Initial commit: OpenMAIC AI classroom platform Next.js 16 App Router application with: - AI-powered interactive classroom generation from PDF/text requirements - Multi-agent discussion system (teacher, students, assistant roles) - Provider abstraction for LLM, TTS, ASR, image, video, web search - Local-first data architecture (IndexedDB/Dexie) - i18n support (zh-CN/en-US) - Structured logger, SSRF guard, lint/build passing (0 errors) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> | 7 个月前 | |
feat: add Brave Search and Baidu Search integration Add Brave Search and Baidu Search providers with Baidu sub-source support, provider configuration updates, tests, and docs links. | 5 个月前 | |
Initial commit: OpenMAIC AI classroom platform Next.js 16 App Router application with: - AI-powered interactive classroom generation from PDF/text requirements - Multi-agent discussion system (teacher, students, assistant roles) - Provider abstraction for LLM, TTS, ASR, image, video, web search - Local-first data architecture (IndexedDB/Dexie) - i18n support (zh-CN/en-US) - Structured logger, SSRF guard, lint/build passing (0 errors) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> | 7 个月前 | |
[codex] Add Amazon Bedrock LLM provider support (#538) * Enable AWS-hosted text models without exposing ambient credentials Rebase the Bedrock provider onto current main and close the review-requested security, credential lifecycle, usage attribution, configuration, and metadata gaps. Constraint: Bedrock may use ambient AWS credentials only when explicitly enabled by the server operator Rejected: Trust client-supplied provider types | permits built-in keyless IDs to reach Bedrock credentials Confidence: high Scope-risk: moderate Directive: Keep provider ID/type validation at both request resolution and model construction boundaries Tested: targeted Bedrock/resolver/config/usage tests; TypeScript; ESLint; i18n alignment; production build Not-tested: Full suite has 27 failures in unchanged quiz/runtime and runtime/chat-storage tests * Give Bedrock a recognizable provider identity Use the official AWS architecture service icon so Bedrock no longer falls back to the generic provider cube in settings and model selection. Constraint: Preserve the AWS-provided artwork without redesigning the service mark Confidence: high Scope-risk: narrow Tested: SVG XML validation; provider unit test; TypeScript; ESLint; production build --------- Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn> | 2 个月前 | |
[codex] Add Bocha web search provider (#524) * Add Bocha web search provider * Address Bocha web search review feedback | 5 个月前 | |
feat: add Brave Search and Baidu Search integration Add Brave Search and Baidu Search providers with Baidu sub-source support, provider configuration updates, tests, and docs links. | 5 个月前 | |
Initial commit: OpenMAIC AI classroom platform Next.js 16 App Router application with: - AI-powered interactive classroom generation from PDF/text requirements - Multi-agent discussion system (teacher, students, assistant roles) - Provider abstraction for LLM, TTS, ASR, image, video, web search - Local-first data architecture (IndexedDB/Dexie) - i18n support (zh-CN/en-US) - Structured logger, SSRF guard, lint/build passing (0 errors) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> | 7 个月前 | |
Initial commit: OpenMAIC AI classroom platform Next.js 16 App Router application with: - AI-powered interactive classroom generation from PDF/text requirements - Multi-agent discussion system (teacher, students, assistant roles) - Provider abstraction for LLM, TTS, ASR, image, video, web search - Local-first data architecture (IndexedDB/Dexie) - i18n support (zh-CN/en-US) - Structured logger, SSRF guard, lint/build passing (0 errors) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> | 7 个月前 | |
Added ComfyUi to has Image Provider (#850) * feat: integrate comfyui workflows into image provider selection * style: format code with prettier * fix: align i18n keys for russian locale * feat: implement requested changes and format code * feat: implement requested changes including renaming default workflow --------- Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn> | 3 个月前 | |
feat(config): openmaic.yml, capability slots and server-side model configuration (RFC #1701) (#1765) * ci: run CI for the provider-config integration branch Development of the provider configuration RFC (#1701) lands on integration/provider-config; build it on push and run CI for pull requests into it, as with earlier integration branches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * feat(config): capability slot registry and stage-to-slot mapping (#1726) * feat(config): capability slot registry and stage-to-slot mapping First P0 step of the provider configuration RFC (#1701, tracked in #1725): define the capability slot forest and map every LLM stage key to exactly one slot. Pure data with no callers yet, so there is no behavior change. Requirements are checked on the slot that declares them and are not passed down to child slots, so agent.title can use a model without tool calling even though agent requires it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * test(config): pin every stage destination and the capability roots Review found that the station and root assertions derived their expectations from the module under test: re-pointing a single-stage station or dropping a capability root still passed. The tests now compare against an independently written stage table and root list, and pin agent.title as config-only. Also document that browserless outlines still run on the generate-classroom model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * test(config): pin slotForStage and full slot lineages Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(config): provider presets and the openmaic.yml schema (#1727) * feat(config): provider presets and the openmaic.yml schema Second P0 step of the provider configuration RFC (#1701, tracked in #1725). - lib/config/provider-presets.ts: one preset per built-in registry entry, plus the token plans as multi-capability presets whose stage recommendations become slot recommendations. Registry ids that collide across capabilities get explicit preset ids. - lib/server/model-config/openmaic-yml.ts: parse and validate the operator's openmaic.yml (or the file named by OPENMAIC_CONFIG): ${VAR} interpolation, strict schema, and cross-checks that every assignment names a declared provider whose preset offers the slot's capability. Every problem is reported with its path. - instrumentation.ts: an invalid file refuses to start. Without a file nothing changes, and nothing resolves models through the file yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): harden openmaic.yml validation after review - Look providers up by own key only, so "constructor:m" is not taken for a declared provider. - Refuse non-mapping objects YAML produces (an unquoted timestamp becomes a Date that the schema would accept as an empty object), and refuse a YAML alias that refers back to itself instead of overflowing the stack. - Accept lowercase variable names; refuse a "${" with no closing brace, checked on the text as written, never on substituted secrets. - Cross-check the entries that are valid on their own even when the schema rejects others, so every problem is reported at once without duplicate errors for declared-but-invalid providers. - Name the three valid assignment shapes when a value has none of them. - Test the boot path: an invalid file exits with code 1 before any schedule starts; a valid one boots. - Ignore a root openmaic.yml in git. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): keep secrets out of openmaic.yml diagnostics Review round 2: - Read only variables the environment itself has, as non-empty strings: ${constructor} or an inherited value is not a variable. - Never print a value that came from ${VAR}, and report YAML syntax errors by reason and position without js-yaml's source excerpt, which can quote a literal key. - Keep the base URL requirement on presets: SearXNG from its registry entry, and Azure OpenAI and self-hosted MinerU, which have no usable default endpoint. - Check slot names and valid model references even inside an entry that fails the schema, and report a failed placeholder once rather than again as an empty value. - Boot test for the no-file case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * refactor(config): validate openmaic.yml in two phases Rounds 1-3 of review kept finding edge cases in one feature: running the cross-checks on the half-valid parts of a file the schema had rejected, so that every problem showed at once. It hid problems inside a rejected entry, lost a __proto__ slot key, and built misleading provider errors from failed placeholders. That feature is gone. Validation now runs in two phases. Phase 1 is the document itself: placeholders, key names (checked on the document as written, since the schema drops a __proto__ key), and the schema. Any problem there stops before phase 2, the references between entries, which therefore only ever sees a fully valid file. Each phase reports all of its problems, one per path. YAML syntax errors now report only their position: js-yaml's reason can quote source text too (an alias or tag name), not just its excerpt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(api)!: generate-classroom takes requirement + uploaded materials; capabilities follow server config (#1728) Narrows POST /api/generate-classroom to { requirement, materialIds? }; capabilities follow server configuration; materials go through the owner material library (upload, reference by id, delete); new GET /api/generate-classroom/capabilities; skill docs updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(config): resolve a slot through the configuration layers (#1729) * feat(config): resolve a slot through the configuration layers Third P0 step of the provider configuration RFC (#1701, tracked in #1725). resolveSlot walks from a slot to its capability root and returns the first assignment it meets, consulting the deployment layer (openmaic.yml, locked) before the workspace layer at every node. An explicit null disables the subtree; nothing assigned up to the root is unassigned, with no fallback to any vendor. The result carries the provider, preset, registry entry, effective base URL, key, model, call options, fallback, where it was resolved and whether it is locked, and the requested slot's own requirements checked against the model catalogue (met, unmet or unknown). Pure and uncalled for now, so there is no behavior change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): lock only written slots and trust the catalogue only where it applies Review round 1: - locked now means the requested slot itself is written in the deployment layer. Inheriting a deployment value does not lock a slot, since the workspace may still assign it (source and resolvedAt still report where the value came from). - A custom OpenAI-compatible endpoint borrows the OpenAI registry for transport only, so its preset no longer trusts that model catalogue: requirements there resolve to unknown. - The fallback is checked against the slot's requirements too, and the result exposes fallbackRequirements so retries can refuse it. - A malformed reference fails with a path-qualified SlotResolutionError that does not echo the value, which is often a misplaced key. - Requirement tests use named catalogue models instead of picking one from the catalogue at run time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): order layers deployment-first and keep references out of errors Review round 2: - resolveSlot no longer depends on the order its caller passes the layers in: deployment layers always come first, for assignments and provider lookup alike. - Resolution errors name the path and the preset, never a value taken from the reference, so a key pasted into the provider position does not end up in a log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * test(config): cover catalogue aliases and fallback capability errors Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(classroom)!: save server-generated classrooms through server persistence (#1730) Server-side classroom generation saves the finished course create-only into the request owner's library with media in the owner's asset pool; jobs move to PostgreSQL with owner-scoped polling; /api/classroom is removed; legacy data/classrooms files are imported once in the background. Adds @openmaic/storage 0.36.0 PgAssetStore.releasePending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(config): translate the legacy configuration into a deployment layer (#1731) * feat(config): translate the legacy configuration into a deployment layer Deployments without openmaic.yml keep working: provider variables, server-providers.yml, DEFAULT_MODEL, MODEL_ROUTES and MODEL_FALLBACK are translated into the openmaic.yml shape and serve as the deployment layer for resolveSlot. - Providers: each configured entry becomes a provider under its preset id; force-disabled entries are left out; AliDocMind's key pair goes into credentials. - DEFAULT_MODEL goes on the llm root. Every other slot that serves stages gets what those stages used before (their route, or else the default model), written only where inheritance would give something else, so an unrouted stage under a routed parent stays on the default model. - Stages that now share a slot but had different routes, models whose provider has no server configuration, and stages that used the browser's model but would now inherit a server one become startup notices, never failures. - MODEL_FALLBACK attaches to every chat assignment without its own. - When openmaic.yml exists it wins, with a notice if legacy variables are also set. Nothing reads the layer yet; routes switch over in P1 (#1725). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): follow the agent driver's rules and keep notices value-free Review round 1: - The agent never used DEFAULT_MODEL: it needs a valid maic-agent-driver route and is off otherwise, so the translation writes agent: null where it would inherit a server model (and a notice for a route that does not work today). Unrouted conversation titles reuse the driver's model with thinking off, as the title generator does. - Notices name registry ids only; a credential pasted into a model variable is never repeated. - Provider entries and route options are checked against the new schema and left out with a notice (field names only) instead of producing a file that would not parse; driver-only options are dropped elsewhere. - A route with its own fallback never falls back to MODEL_FALLBACK, even when that fallback cannot carry over; the retry model is part of the comparison with the inherited value. - Force-off switches without a configured entry are reported, a legacy configuration made only of them is detected, and translating one adds a deprecation notice. - loadDeploymentLayer reads the process environment and working directory like the legacy loaders, instead of taking parameters it could only partly honor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): compare routes by behavior, validate references, name only known ids Review round 2: - Stages sharing a slot are compared by what the route changes for them (model, thinking, fallback), with bare ids read as openai models and a route to DEFAULT_MODEL counted as no route; api and contextWindow are inert outside the driver. - A model reference carries over only when it names a provider from the providers section and forms a valid reference. - Section keys and force-off ids are named in notices only when they are registry ids with a preset. - Retries now follow the slot; where a call site picked its retry model by another label (scene types under scene-content, browserless generation), the change is reported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): chat references need a chat entry; one notice for retries Review round 3: - A model reference carries over only when its provider was translated from the providers section, not merely declared by another section under the same id. - Routes are compared by the retry model that takes effect, so leaving out a fallback equals repeating MODEL_FALLBACK. - The per-call-site retry notices kept missing cases (streaming and calls outside server-managed routing never retry today). They are replaced by one notice, given whenever a retry model is configured, that retries now follow the slot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * refactor(config)!: translate only providers and the default model (#1732) * refactor(config)!: translate only providers and the default model MODEL_ROUTES does not map one to one onto slots: several stages share a slot, the agent driver and conversation titles have rules of their own, and retries are picked by call-site labels. Emulating that took most of the translation and still ended in notices that are easy to miss. The translation now carries over only the unambiguous part: providers, DEFAULT_MODEL as the llm root and MODEL_FALLBACK as its fallback (the agent stays null, as it never used DEFAULT_MODEL). A deployment that sets MODEL_ROUTES without openmaic.yml gets LegacyRoutesError asking it to write the per-stage models as slots. loadDeploymentLayer is not wired into startup yet; that happens when routes switch to resolveSlot (P1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): say what a dropped MODEL_FALLBACK loses Without DEFAULT_MODEL there is no assignment to hold MODEL_FALLBACK, and calls that retried on it (server providers picked in the browser) stop retrying; the notice says so. thinkingSchema is module-private again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(persistence): workspace model configuration with keys encrypted at rest (#1733) * feat(persistence): workspace model configuration with keys encrypted at rest The store behind the web settings of RFC #1701 (#1725, P1): one row per workspace (owner) in workspace_model_config, the same shape as openmaic.yml without policy. - Provider secrets (apiKey, credentials) are sealed per provider with AES-256-GCM under OPENMAIC_SECRET_KEY, bound to the provider id, and never stored in the config column. Without the variable, a secret is created once in data/instance-secret.key (the Docker volume). - A secret sealed under another instance secret is reported as unreadable ("enter again"), and a save that brings no new secret for that provider keeps it, so a misconfigured secret cannot destroy keys. - Saves replace the document with a compare-and-swap on the revision, after the owner's identity lock; two concurrent first saves cannot both land (PostgreSQL contract test). - A claim moves the anonymous configuration to the account unless the account has its own (core participant, order 900). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(persistence): publish the instance secret atomically; force the races in tests Review round 1: - The generated secret is written and flushed under a private name, then published with an exclusive link, so no process can read it half written and exactly one of two starting processes creates it. A file that is not a complete generated secret (empty, truncated) is refused instead of deriving a key anyone could compute. - The PostgreSQL test holds both first saves at their insert until both have read the missing row, and holds the row lock for the update case until both saves queue behind it (checked by backend pid, not by any lock waiter in the database). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(persistence): verify the written secret; require full GCM tags Review round 2: - The generated secret is written in full, read back and checked before it is published; the temporary file is removed on any failure. - Sealed values open only with a 12-byte IV and a full 16-byte tag (authTagLength), so a shortened tag cannot weaken authentication. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(config): resolve slots at request time over deployment, workspace and defaults (#1734) * feat(config): resolve slots at request time over deployment, workspace and defaults The runtime half of RFC #1701 resolution (#1725, P1): - deploymentConfig(): the deployment layer, loaded once per process. The legacy translation now splits into the deployment's providers and a separate default layer (DEFAULT_MODEL, MODEL_FALLBACK) that locks nothing and ranks below the workspace, as DEFAULT_MODEL ranked below the model a user picked. - workspaceLayer(owner) reads the web settings; requestWorkspaceId(req) names the request's owner (none for an owner minted by the request). - lookupSlot walks deployment and workspace over the whole tree first; the defaults are a second walk, so a default on a child never outranks a workspace choice higher up. - resolveStageModel builds the language model: the configured slot, else what the request still names the old way (deprecated), else the defaults, else a loud error; a slot turned off fails whatever the request names. A workspace provider's endpoint is checked like a caller-supplied one and gets the transport that refuses redirects. - resolveSlot gains the default source, and reports which layer declared the provider, its proxy and credentials. Nothing calls it yet; call sites switch next. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): keep request identity and workspace endpoints inside their bounds Review round 1: - A refused owner credential throws InvalidOwnerCredentialError (401) instead of resolving with the deployment's models and keys. - A retired request owner gets no workspace: canonicalizing it would hand an old anonymous cookie the claiming account's settings. Only background work, which names a stored owner, is forwarded. - A workspace provider may not be Amazon Bedrock (which falls back to the server's AWS credential chain) or set a proxy (which routes around the checked, redirect-refusing transport); the deployment still may. - Test seams for the deployment config and the workspace loader, and tests for identity, forwarding and caching. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): read only the named owner's settings; refuse unmet requirements Review round 2: - workspaceLayer reads exactly the owner it is given. Forwarding through a claim let a request whose owner was claimed between its check and the read see the account's settings; background work passes the owner it works for now instead. - A model the catalogue says does not meet the slot's requirement is refused before anything is built (SlotRequirementError), without consulting the request. - Tests: a database failure fails the call without falling back, a workspace provider without a key never gets the deployment's, and the transport each provider source gets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * test(identity): exempt the model settings lookup from cookie forwarding requestWorkspaceId resolves the owner only to pick whose model settings a generation call uses, and returns no response of its own, like the vision prompt helper already listed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(config)!: resolve every LLM call through its capability slot (#1735) * feat(config)!: resolve every LLM call through its capability slot The LLM call sites of RFC #1701 switch to slot resolution (#1725, P1): - resolveModel resolves a stage through its slot for the request's or job's workspace: the deployment and workspace configuration first; the model and key a request names (x-model, x-model-routes, body fields) only for a slot left unassigned, deprecated; then the defaults an older deployment set with DEFAULT_MODEL. Routes that read headers get this through resolveModelFromRequest; the chat routes pass their workspace; background work (agent runs, titles, generation jobs) passes the owner it works for now. - Retries follow the slot: a slot-resolved model carries its slot's fallback (lib/ai/model-fallbacks.ts), which callLLM and the outline stream use; only a model from the request path still retries on MODEL_FALLBACK. A fallback that cannot meet the slot is not used. - The agent driver resolves the agent slot: tool calling required, api defaults to openai-completions, thinking.effort still refused. Conversation titles resolve agent.title with thinking off unless the title slot sets it. - /api/generate-classroom resolves each step through its slot for the job's owner, its outline through course.outline. - MODEL_ROUTES is gone: loadDeploymentLayer runs at startup and a server that still sets it without openmaic.yml refuses to start. The pbl-chat and maic-agent stage keys, which nothing resolved, are removed. BREAKING CHANGE: MODEL_ROUTES is no longer read; write per-stage models as slots in openmaic.yml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): a slot fallback needs no serverManaged stamp; follow claims per stage Review round 1: - callLLM arms a model's attached slot fallback whether or not the caller passes serverManaged (the PBL agents pass none); the stamp still gates MODEL_FALLBACK on the request path. - /api/generate-classroom resolves the owner the job works for now at each stage, so stages after a claim follow the moved settings. Streaming calls (agent driver, classroom chat, PBL streams) still do not retry on a fallback model, as before this change; that is the #1725 item on retries at every call site. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(config): resolve media and tool capabilities through their slots (#1737) * feat(config): a provider-only reference names the provider's default model Search and document providers mostly have no model to pick, so a slot may name just the provider (`webSearch: tavily`); the target then has no modelId and the adapter uses its default. Chat slots still need providerId:modelId, checked in openmaic.yml and at resolution. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * feat(config): resolve media and tool capabilities through their slots The capability routes and server paths of RFC #1701 (#1725, P1): text to speech, speech recognition, images, video, web search and document extraction resolve through their slots, like language models. - lib/server/model-config/media.ts: the configured slot (deployment, then workspace); else the provider a request names the old way (deprecated); else the legacy defaults; else a loud error. A slot turned off fails whatever the request names, and while the legacy <CAP>_<VENDOR>_ENABLED=false switches are in effect a switched-off provider stays off whoever assigns it. A workspace endpoint is checked like a caller-supplied one and may not use a proxy. - The legacy translation assigns the media roots the provider the server picked when a request named none (first configured; web search by its old priority; DEFAULT_IMAGE_PROVIDER for images), with the first pinned model. - Routes: /api/generate/{image,video,tts,voice}, /api/transcription, /api/web-search, /api/parse-pdf and /api/extract-document. A TTS voice applies only to the provider it was chosen for; voices register on the tts slot's provider. - Server paths: agent image and video generation, scene narration, the voice catalog and registration, web search and fetch_url, material extraction (the document slot's service, the asr slot for local transcription), browserless classroom media, and the generation capabilities reported by /api/health and the classroom capabilities endpoint. - A provider-only reference (`webSearch: tavily`) names the provider's default model; chat slots still need providerId:modelId. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): keep slots authoritative on every media path - Workspace providers reach media, search and document services only at their preset's endpoints; a custom endpoint or proxy is deployment-only (403 INVALID_URL). - A configured or turned-off document slot decides the extraction service: deprecated request fields may only pick a self-contained extractor, and legacy operator credentials are no longer reached. - Request paths resolve their own workspace without following a claim; background extraction still does. - A configured TTS slot keeps its own model; legacy pins apply only on the deprecated and default paths. - Local transcription and agent voice registration keep the connection's network policy flags. - The agent's web search forwards the whole resolved configuration. - An unusable DEFAULT_IMAGE_PROVIDER leaves the image slot unassigned with a notice instead of switching vendors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): public presets only for workspaces, plan default models - A workspace preset whose default endpoint is on the server's own network (self-hosted TTS, ASR, image) is deployment-only, and a workspace provider's endpoint runs under the public-only policy. - A provider-only reference to a token plan means the plan's own default model for that capability. - On the legacy default provider, the request's image, video and ASR model still applies through its allowlist, and image/video without a model is still MISSING_MODEL. - An asr assignment a workspace may not use no longer blocks document extraction, and a refused document service answers 403. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): keep legacy search and voice choices on their old paths - Classroom search honours the provider and key a request names while the webSearch slot is unassigned; only /api/web-search prefers the operator's configured backend, as before. - A TTS request's voice applies unless the request chose it for another provider. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): legacy search model and capability discovery errors - On the legacy default search provider, the request's search model still applies through the server's pins. - Capability discovery answers a refused credential (401) or a workspace service it may not use (403) instead of 500. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): classroom submission refuses a workspace service with 403 A classroom submission whose materials would go to a document or speech service the workspace may not use answers 403 INVALID_URL instead of 500, and its material check resolves the request's own workspace without following a claim. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): workspace image and video providers run public-only A workspace-configured image or video provider now uses the strict public transport for its requests and for the redirects its clip download follows, whatever ALLOW_LOCAL_NETWORKS says; the operator's own providers and the deprecated per-request path keep their policies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * Revert "fix(config): workspace image and video providers run public-only" This reverts commit 68021ded. A workspace provider cannot choose an endpoint for media, search or document services at all: a custom base URL or proxy is refused, and so is a preset whose default endpoint is on the server's own network. What remains is a preset's fixed public endpoint, the same one a deployment default uses, so these calls keep the operator's transport policy like every other preset endpoint, and the connection no longer claims a user-typed endpoint (userEndpoint is false). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): /api/web-search without a provider searches as before A deprecated web search request that names no provider again means the server's configured provider, else the default one with the request's key, while the webSearch slot is unassigned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): browser speech recognition does not count for extraction An asr slot assigned to speech recognition that runs in the browser gives server-side extraction no transcription service, so audio uploads are not advertised or accepted for extraction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): /api/parse-pdf honours an explicit local parser A request that asks for a self-contained extractor (local parsing) parses the PDF locally and sends it to no document service, whatever the document slot names, as /api/extract-document already did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * test(classroom): give the persistence contract its media slots The generated-classroom PostgreSQL contract configured its image and TTS providers through the legacy provider mocks, which slot resolution no longer reads; it now sets the deployment's slots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(llm): streaming calls fall back on their slot's fallback (#1739) * feat(llm): streaming calls fall back on their slot's fallback A stream resolved through a slot that fails before any content (the request is refused, or the first part is a retryable error) runs once on the slot's fallback model. A failure after content has started still reaches the caller. The outline stream keeps its own retry and fallback handling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(llm): the stream fallback is the call's last attempt, before any content - The primary's own SDK retries run first; the fallback runs once, as the last attempt, and a failing fallback is not retried. - No fallback once content reached the caller in any step (a tool that ran must not run again); after a fallback took over, later steps stay on it. - The fallback gets the thinking options built for its own provider and model, not the primary's. - Usage of a stream with a slot fallback is recorded per step, against the model that served the step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(llm): stream fallback recognises transient error payloads Providers send a stream's error part as a plain { type, message } payload rather than an error; a transient type (overloaded, rate limited, server error) now counts as retryable for the slot fallback, while authentication and request errors still do not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(config): model settings API for workspaces (#1740) * feat(config): model settings API for workspaces GET/PUT /api/model-config read and edit a workspace's slots and providers: every slot with its own assignment, effective model, source and lock state; deployment providers read-only and without credential details; workspace keys write-only and masked. Edits are checked against the whole configuration and a revision. The view carries the preset catalogue a workspace may add providers from, and each provider's models per capability, so the settings UI needs no registry code of its own. Workspaces cannot add Bedrock, self-hosted media/search/document presets or custom endpoints for anything but chat. POST /api/model-config/import merges settings a browser kept, item by item under the same checks, never replacing what the workspace or deployment already has. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * refactor(config): preset ids as client-safe data The preset id tables move to lib/config/preset-ids.ts, which imports no registry, so browser code (the settings import) can name presets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): tighten the settings API's views and checks - Effective targets name an endpoint only for the workspace's own providers; a deployment's endpoints never reach a response. - A workspace provider of a local model server preset (whose default endpoint is the server's own network) must name its own endpoint. - Every change is checked against the stored shape, so an import skips a malformed item instead of failing the batch, and PUT validates its whole body (400, never 500). - Clearing a key deletes it even when the instance can no longer open it. - A workspace provider with its own endpoint offers chat models only, and an OpenAI-compatible provider offers only the models it lists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): no credentials in endpoints; key-pair presets stay in the yml - A workspace base URL cannot carry a username or password (they would be stored and shown in the clear), and views never show credentials a stored endpoint carries. - Presets that authenticate with a key pair (AliDocMind) are not offered to workspaces: the settings take one key per provider. - An import checks each proposed provider on its own, so one malformed provider is skipped instead of failing the batch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): settings neither offer nor accept switched-off providers A provider the operator switched off for a capability (the legacy <CAP>_<VENDOR>_ENABLED=false switches) is left out of the capability lists, refused as an assignment, and shown as invalid where a deployment assigns it, as the calls themselves refuse it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): recommendations name only capabilities a preset still offers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): a provider id follows the reference grammar before it is used Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): the catalogue lists the models a search provider offers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(config): refuse a fallback on a slot whose calls never use one (#1743) * fix(config): refuse a fallback on a slot whose calls never use one Only language-model calls retry on a slot's fallback; a fallback on a speech, image, video, search or document slot was accepted and silently did nothing. Resolution now refuses it by path, so openmaic.yml fails at startup and the model settings refuse it on save. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): openmaic.yml refuses a non-chat fallback at startup The boot check parses openmaic.yml without resolving slots, so the refusal also lives in the file's cross-checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(settings): Models section with the course model map (#1741) * feat(settings): client for the workspace model settings A small client for /api/model-config: it caches the view per page, applies changes against the revision it read, reloads on a stale revision (409 CONFLICT) or a slot the deployment has since locked, and treats a server without persistence (404) as settings managed by the server. Pure helpers behind the settings UI live beside it: slot changes from the card picker (follow, off, a model, a fallback, the media switches), the provider form's change (keys stay write-only: keep, replace or remove), the first-run setup that adds a provider and fills only the empty, unlocked slots with its preset's recommendations, and the layout of the course model map. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * feat(settings): Models section with the course model map A new "Models" section, first in the settings dialog and the one it opens on. It reads and writes everything through /api/model-config: - The model map: a pannable, zoomable canvas with the course pipeline as a two-row serpentine flow, the default language model above the stations that inherit from it, and the page types under the content station. Each card shows the effective model, where it comes from and a lock when the server sets it. Solid edges follow the parent, dashed ones mark a setting of the slot's own. - Editing happens in place: a picker at the card to follow the parent, pick a model a provider offers (or a provider, for search and document slots), turn the slot off, or set a fallback for chat slots. Media switches turn a slot off and back on. - While no language model is configured, the default model's card offers a first-run setup: pick a service, give its key, and its recommended models fill every empty slot. - A Providers tab lists the server's providers (read-only) and the workspace's own, which can be added, edited and removed as the server's policy allows. The generation toolbar's "configure provider" prompt opens this section. Below the sm breakpoint the settings nav becomes a strip above the panel. Strings are translated for all 12 locales. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): keep what a change does not mean to touch - A provider edit carries only the fields the form shows: a hidden model list or endpoint is left out, so the server keeps it; a shown field that is emptied is removed. A pinned model list keeps its field on edit. - A key the server cannot read starts as "replace" (it can also be removed), so a key typed for it is sent instead of the broken one being kept. - Switching a media slot off remembers what it held; switching it back on restores that assignment (or nothing of its own), and when that is not known the caller asks instead of clearing the slot to the default. - The first-run assignments are checked against the provider as the server answered it: a provider with its own endpoint serves chat only, a model list the user gave wins over the recommended chat models, and `llm` is one of the provider's own chat models. The filling can be retried against a reloaded view, and a refusal keeps its reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): recoverable first-run, root clear action, keyboard on the map - A first-run setup that added its provider but could not assign it (a stale revision, say) reports to the section, which keeps a notice with the provider and a way on (assign its models again, or add them under Providers) through any reload, instead of losing it with the form. - The picker of a root slot with a setting of its own offers to clear it, leaving the server's value or default. - A card's switch restores what the slot held; when that is unknown it opens the picker. - Keyboard focus on a card outside the view pans the map to it, and the arrow keys pan the map while it has focus. - The provider form offers only replace or remove for an unreadable key, and says that a provider with its own endpoint serves chat only. Component tests drive the provider form, the switches, the root picker, arrow-key panning and a first-run setup that meets a conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): keep picker row labels whole; one unreadable-key notice A long note no longer truncates the row's label in the slot picker, and the provider form stops repeating the unreadable-key warning the row already shows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): a lost answer to a write is an outcome, not a throw When the server takes a change but its answer cannot be read (a truncated body, a dropped connection), apply() no longer rejects: it reloads the view to reconcile a change that may have been saved and returns an `unconfirmed` outcome with the reloaded view. The first-run setup goes on when the reloaded view has the provider it added, counts a slot write whose answer was lost as done when the reload shows the default model set, and says so otherwise. Busy states in the provider form, provider removal, first-run setup and its notice are reset in `finally`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): keyboard roving in the slot picker The picker's rows are plain buttons (pressed for the current choice) in a group, not listbox options without the keyboard model those promise. The list is one Tab stop, the current choice or else the first row; ArrowUp and ArrowDown step, Home and End jump, Enter and Space choose. The picker opens on that row. The picker's and the card switches' busy states are reset in `finally`, and the component tests cover the lost-answer first-run paths and the picker keyboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): a write lost in transport is unconfirmed, and first run can resume A write whose request fails in transport may still have been saved, like one whose answer is lost: apply() now reloads the settings and returns an `unconfirmed` outcome in both cases, with the reloaded view when that read worked. A first-run setup whose provider add cannot be confirmed (the reload failed too) keeps a notice that says so, and "Check again" reads the settings and either assigns the provider's models or says it was not added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): retry an unconfirmed provider add under the same id The add-provider form keeps the id it tried. When the answer was lost it closes if the reloaded settings show that provider, and otherwise retries under the same id, so a save that did land is updated rather than joined by a second provider; a genuinely new add still gets a fresh id. Component tests cover this and the first-run paths where the provider add fails in transport after the server saved it, or cannot be confirmed at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): 5xx answers to writes are unconfirmed; reads never go back A server error or a gateway timeout can come after a write was saved, so a 5xx answer to a write is treated like a lost answer: the settings are read again and the outcome is `unconfirmed` with the reloaded view. A 4xx is still a refusal, without a reload. Reads and adopted write answers are numbered: a read's answer is dropped when a later read started or a write's answer was adopted after it began, or when its revision is older than the view held. The reads that must see a write just made (after an ambiguous write, a conflict, "Check again") start a fresh request instead of joining one begun before the write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): first run is done only once the default model is set A slot write that succeeds without setting `llm` (another session turned it off meanwhile, so only media slots were filled) no longer counts as a done setup: the notice stays and says the default model is still missing, for the user to pick on its card. Test views now carry `llm` resolved the way the server answers it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): the revision decides which settings view is newer A view with a lower revision than the one held never replaces it, whether it comes from a read or a write's answer, and one with a higher revision always does, whenever it arrives; the order in which reads and adoptions were numbered only breaks ties at an equal revision (and decides while no view is held, so a read begun before the view was forgotten does not bring it back). A write whose answer is older than a view read meanwhile returns the view that is current. `adopt` takes a view or null (forget it) and is part of the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): send each change against the view it was worked out from apply() takes the view a change was computed from and sends that view's revision, rather than the revision of whatever view is held when the write goes out. A retry of the first-run assignments that waits for a reload in flight is therefore refused (409) when the reload shows the settings changed, instead of overwriting a slot set meanwhile; the reload then feeds the next attempt. The slot picker, the card switches, the provider form, provider removal and the first-run setup all pass the view they showed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): a provider edit sends only what changed, and rebases on a conflict The provider form keeps the basis of an edit: the provider as the edit began and the view it came from. A save sends only the fields changed against that basis (a key-only edit sends the key), against the basis view's revision. When the provider changed elsewhere meanwhile (409), the edit moves onto the reloaded provider, keeping only what the user changed, and the form says the provider changed before it is saved again. A 409 now returns the reloaded view to work the change out again from. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): a refused provider add keeps its draft and picks a free id Only an add whose outcome is unknown (a lost answer, a transport failure, a 5xx) is reconciled by looking for its id in the reloaded settings. A confirmed refusal (a 409, another tab having added the same preset under the same id meanwhile) saved nothing of ours: the form keeps the draft, shows the conflict, and the retry uses an id still free in the reloaded view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * refactor(settings)!: server-side model settings only; import browser settings once (#1744) * feat(settings): one-time import of browser model settings The settings store's migration to version 5 builds an import proposal from the provider state earlier builds kept in the browser (keys, custom endpoints, the chosen model, token plan enrollment, per-capability selections) and keeps it under its own localStorage key. Once the store has hydrated, the proposal is posted to /api/model-config/import: a 2xx answer removes it (and the keys) from the browser, 400 drops it, and anything else keeps it for a later load. Per-stage routes are not imported. Clear Local Cache keeps a proposal still waiting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * refactor(settings): stop sending provider data from the browser Every request now leaves model and provider choice to the server, which resolves it through the workspace's capability slots: no model, key or base URL headers (x-model, x-api-key, x-model-routes, x-image-*, x-video-*), no provider, key or thinking fields in chat, TTS, voice registration, transcription, web search or document extraction bodies. The server keeps accepting them until they are retired. What the client still needs to know is read from the /api/model-config view (lib/model-settings/capabilities.ts): whether a language model is set up, which provider the tts slot names (browser speech plays locally; voice lists follow that provider), whether speech input, image, video and web search are available, and the course model the toolbar shows or edits through the settings API. The scene concurrency comes from /api/health. The unused TTS config popover and getCurrent*Config helpers are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * refactor(settings)!: remove the browser provider state and its settings UI The settings store keeps only the user's own preferences (playback, narration voice and speed, speech input language, outline review, agents, layout). Providers, keys, base URLs, the model choice, thinking settings, per-stage routes, token plan enrollment and seeds, the per-capability provider configs and selections and their on/off switches are gone; the version 5 migration sets them aside for the one-time import and drops them. The narration voice now records the provider it was picked for and applies while the tts slot names it. The Token Plan, Model Services and Course Model sections and their components are removed, with apply-token-plan, the server provider sync (ServerProvidersInit, fetchServerProviders) and helpers only they used. A Voice section keeps the per-user parts of the old speech settings: speed, a narration test, the VoxCPM and Qwen voices, and the speech input language, showing which services the workspace uses. The home toolbar picks the course model through the settings API, or shows it read-only when the deployment locks it. e2e fixtures answer /api/model-config instead of seeding browser providers; the managed-provider spec, which covered the removed panel, is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * feat(settings): browser speech recognition while the asr slot is unassigned Speech input worked out of the box through the browser's own recognition, which needs no server provider; it stays available while the asr slot is unassigned, and turning the slot off turns speech input off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): bind the model settings import to its owner and never lose staged keys The import now asks for the browser's legacy import binding and sends X-OpenMAIC-Legacy-Import, so owner resolution refuses it for any owner that does not hold the browser (409 LEGACY_IMPORT_NOT_BOUND keeps the proposal for a later load); the import route joins FENCED_ENDPOINTS. Its completion is the proposal's own key, apart from the course ledger. Staging reports whether it succeeded. When the proposal cannot be written (a full storage, an unreadable proposal waiting), the migration keeps the old settings in legacyModelSettings and every load retries, writing the store back without them once staged. Nothing logged quotes the proposal or an error message: fixed text, item ids, error names. Older shapes are normalised before the proposal is built (version 0 default model, the single TTS model setting, global TTS/ASR model ids, a TTS provider's model field, the flat web search key). Capabilities the user turned off are proposed as off (null), and selected server-configured media providers are named by their preset id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): retry a failed read of the model settings A failed read of /api/model-config with nothing to show is read again with a backoff (2 s up to a minute) until one succeeds, instead of leaving the page without capabilities for the rest of its life. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): send no provider or model with voice registration Voice registration, deletion and auto-registration no longer send providerId or ttsModelId: the server registers with the provider and model the tts slot resolves to. The model still derives the voice id and keys the session memo. The unused OpenRouter model list hook, which called the vendor with a browser key, is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): carry only an explicit speech input off into the import Only `asrEnabled: false` becomes an off slot (`asr: null`): without an asr slot the browser's own speech recognition would take over, which the user had turned off. The TTS, image, video and web search switches were per-browser toggles that availability following the slots replaces on purpose; turning them into lasting workspace nulls would override the deployment's defaults from then on, so they are not carried over. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): gate chat and generation on the slots they resolve Chat and discussion resolve the classroom slot, and course generation its outline, actions and content slots; each is now allowed whenever those resolve to a model, even with the llm root unassigned or off (a child assigned on its own). Settings not read yet still block nothing. The toolbar offers "Set up model" only when a course cannot be generated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): import models the user added to built-in providers A built-in chat provider whose model list held models the user added (ids not in its catalogue) is proposed with those models, listed after the catalogue's: a provider's model list names the chat models it serves, so listing only the added ones would hide the catalogue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): wait for the commit explicitly in the media overlap test The overlap test counted 50 microtasks for the mocked commit to be reached; with the capability read before each pass that is no longer enough, so the test failed alone and left a pass running into the next test. It now awaits a signal from inside the commit, flushes every pending microtask before asserting, and releases and awaits both passes in `finally`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): import models the user added to an enrolled token plan An enrolled plan's chat provider whose model list held models the user added (ids not in the plan's own list) is proposed with them, listed after the plan's, by the same rule as built-in providers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): scope voice registration memos to the tts slot's provider The session memo and in-flight map of auto-voice registration were keyed on the voice and model only, so after the tts slot moved to another backend serving the same model, registration was skipped and synthesis named a voice that backend never registered. They are now scoped to the provider the slot resolves to (provider id, registry entry, endpoint) and the settings revision, captured once per registration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): show the view the model settings import produced After a successful import the page read the settings again, but that read could join the page's first read, still in flight from before the import, and leave the unassigned view in place. The import now hands over the view the import route answered, and adoptNewerView lets any read in flight finish before keeping whichever view is newer by revision, so an older answer cannot land over it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): offer and resolve voices against the tts slot's model With the tts slot on a model that cannot speak some voices (an OpenAI slot on tts-1 and Marin or Cedar, which need gpt-4o-mini-tts), the picker still offered them and synthesis on the slot's model failed. The slot's model (its own, else the provider's default) is now the model voices are offered under and checked against: the voice lists hold only voices it can speak, a persisted voice it cannot speak falls back to a compatible default, and narrator and agent bindings to such a voice are not used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): drop translations only the removed provider panels used 208 keys that only the removed provider settings (model services, token plan, course model config, their dialogs and the TTS popover) used are removed from all 12 locales: API key and base URL fields, provider and model editors, connection tests, per-capability switches and the like. Each was checked to have no remaining reference, literal or through a key prefix built at run time. The TTS enablement locale test keeps the key still in use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): provider options in openmaic.yml, passed to the TTS adapters A provider in openmaic.yml may carry `options`: non-secret, provider-specific settings (a map of names to strings, numbers or booleans; `${VAR}` interpolation applies). They reach the resolved slot target, the media connection and the settings view, and the server's TTS paths (the TTS and voice routes, scene narration, classroom media generation, the voice-clone tools) hand them to the adapter as its providerOptions: over a request's options for a configured slot, under them while the slot is unassigned (the deprecated path). Voice registration follows them too (a VoxCPM backend without runtime registration is refused). Workspace providers cannot set options: they are the deployment's. The legacy configuration had no equivalent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): read the VoxCPM backend from the tts slot's options The voice settings, the voice lists and auto-voice registration assumed the default VoxCPM backend once the browser no longer chose one. They now read the `backend` option of the provider the tts slot resolves to (shown in the settings view), and the registration memo is scoped to the provider's options as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): label the preset choice when adding a provider Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): media planning from the slots; stop when settings are unread The outline route decided whether to plan images and video from the client's x-image-/x-video-generation-enabled headers, and the client sent them from settings it might not have read: a course could be generated silently without media. The route now reads the workspace's image and video slots itself (an explicit `false` header still lets an API client opt out; `true` turns on nothing), and the client no longer sends them. When the model settings cannot be read even after another try, generation stops with a message instead of leaving out narration (the scene fails and generation pauses; the preview's first scene errors), and a media pass or retry stands down without marking anything as disabled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): pick the course model when none is set Without a default model the toolbar showed no picker, so the course model could only be set in Settings. It now offers the picker whenever the workspace may set the llm slot and chat models exist, with nothing selected until one is picked (which sets the slot; the server assigns nothing on its own). The legacy translation's notice for a model whose provider the server does not configure now says each workspace chooses its model in Settings → Models, or to configure the provider and set DEFAULT_MODEL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): import keyless search choices and the Claude search model A keyless search service (Brave) that the user selected with research switched on is proposed as a provider of its preset with the webSearch slot on it; untouched defaults and self-hosted services (SearXNG, whose endpoint only the deployment may set) are not. The model picked for Claude web search is carried as `claude:<model>` instead of being dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): refuse credential-like provider option names A provider's `options` are shown in the settings view, so a name that matches key, secret, token or password is refused with a pointer to apiKey and credentials. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): pin which course slots the generation gate needs courseGenerationUsable requires the outline, actions and a content slot. The agents and research slots are not required, and a test pins it: with course.agents off the roster falls back to the preset agents, and with course.research off web search runs on the raw requirement, so neither request is refused by the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * docs(settings): what the import leaves behind; extraction is a slot The importer README lists what is not carried over and where it is set now: per-stage routes, the per-browser media and research switches, Baidu sub-sources, thinking settings and the VoxCPM backend (options in openmaic.yml). The document extraction README no longer documents the removed store fields and request-level provider fields: extraction is the workspace's document slot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): release the recorder lock when a start is refused startRecording returned early without clearing its lock when speech input was not set up (the asr slot off, or still unknown while the settings loaded) or the browser lacked speech recognition, so every later click did nothing until the component remounted. Each early return now releases the lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): decide research from a successful settings read The home page saved `webSearch: undefined` into the generation session when the model settings could not be read, and the preview researched only on that saved flag, so a transient failure skipped research for the whole generation. Saving the session now needs a successful read (read again after a failure, else it stops with the "settings could not be read" message), and starting or resuming generation decides research again from the current webSearch slot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * test(settings): type the research decision session Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(config): a policy without workspace providers also stops using them (#1750) * fix(config): a policy without workspace providers also stops using them `policy.allowWorkspaceProviders: false` only stopped workspaces from adding or editing providers; ones added before kept serving their assignments. Resolution now leaves them out, with the workspace assignments that name them (an assignment whose fallback alone names one keeps its model), so the policy takes effect for existing workspaces too and the settings view shows what the calls use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): policy keeps shared provider ids and refuses new references - A workspace provider id the deployment also declares resolves to the deployment's provider, so its references are kept under the policy. - The model settings refuse a new assignment to a workspace provider the policy no longer allows, while other edits leave dormant ones as they are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): settings validate a workspace as the policy lets the calls see it Assignments the policy leaves dormant are not checked on unrelated edits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * docs(config): openmaic.yml, capability slots and the model settings (#1742) * docs(config): document openmaic.yml, slots and the model settings The configuration docs are rewritten around openmaic.yml: providers, the slot tree and its inheritance, assignment forms, provider-only references, fallbacks, turning a capability off, locks and the workspace model settings, policy, OPENMAIC_SECRET_KEY, what workspaces may add, the deprecated request fields, and migration from provider variables, server-providers.yml, DEFAULT_MODEL/MODEL_FALLBACK and MODEL_ROUTES (with the stage-to-slot table). The environment-variable reference stays as the legacy configuration. Deployment, supported models (a preset catalogue), getting started and VoxCPM2 point to openmaic.yml; the READMEs' quick configuration and the agent runtime example use it; the openmaic skill tells agents to configure models through openmaic.yml or the model settings. openmaic.example.yml is a starting point, and a test parses it and every documented openmaic.yml example with the real schema. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * docs(config): translate the openmaic.yml documentation Brings the zh-CN, zh-TW, Japanese, Russian and Arabic pages in line with the English configuration, deployment, supported models, getting started and VoxCPM2 pages: the same sections, examples and tables, with the legacy environment-variable reference kept as before under its own heading. Also fixes the zh-CN VoxCPM2 link to the TTS section, which pointed at the English anchor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * docs(config): match the settings UI names, set a model in the quick start - Configuration: the settings tabs are "Model map" and "Providers", the lock reads "Set by the server", and the first-run setup is "Connect a service". Browser import: keys are removed from the browser once imported (a failed import keeps them), and speech input that was switched off becomes asr: null. - Getting started: the browser no longer sends a model, so the quick start sets one, with a minimal openmaic.yml or DEFAULT_MODEL next to the key, and names Connect a service in Settings > Models as the path without a file. - openmaic.yml is in .dockerignore so a file with inline keys is never baked into an image; the deployment page says to mount it at run time. All six locales are updated where the text changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * docs(config): scope fallbacks, trim the example, translate volume repair - Fallbacks apply to language-model slots (llm and the slots below it); a fallback on a media, search or document slot is refused at startup or on save. - openmaic.example.yml is copied as-is by the READMEs, getting started and the skill, and startup refuses any unset ${VAR}: it now has one active provider (OPENAI_API_KEY) and the llm slot, with every other provider and slot as a commented optional block. The copy instructions say so. - The translated deployment pages gain the root-owned volume repair paragraph and its chown command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * docs(config): configure self-hosted and custom media endpoints in openmaic.yml Workspaces cannot add custom non-chat endpoints or self-hosted media presets, so the docs no longer send users to the browser settings for them: - Configuration (legacy reference): custom OpenAI-compatible TTS and ASR endpoints and ComfyUI are declared in openmaic.yml with their baseUrl and assigned to tts / asr / image; custom chat endpoints are an openai-compatible provider in openmaic.yml or the Providers tab. The claim that ASR configuration stays in client settings is gone. - VoxCPM2 (page and READMEs) is configured in openmaic.yml, with the legacy variable as the fallback; the per-browser Base URL option is removed. - MinerU in the READMEs is a document provider in openmaic.yml. - Deployment and ComfyUI: providers declared in openmaic.yml are server-managed and may use private endpoints without ALLOW_LOCAL_NETWORKS, which only matters for endpoints a user types (chat base URLs in the model settings, deprecated request fields). All six locales are updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * docs(config): document provider options and the VoxCPM2 backend option - Configuration: the provider fields table documents `options`: non-secret, provider-specific settings (string, number or boolean values, ${VAR} allowed), shown in the model settings so never a key, and deployment-only. - VoxCPM2 (page and READMEs): the backend is chosen with options.backend on the provider in openmaic.yml (vllm-omni by default, python-api, nano-vllm), not in the browser settings; voice registration works only on vllm-omni, and the troubleshooting row points at options.backend. - The example test skips openmaic.yml blocks that set provider options until the schema on this branch accepts them (marked TODO). All six locales are updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * test(docs): check the VoxCPM2 examples now that provider options exist Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * docs(config): describe the browser import, Voice settings and web search as released - Browser import: the proposal and its keys are removed after any 2xx (even with skipped items, which are kept nowhere), and after a 400 or an unreadable proposal; it stays for another attempt only on 401, 404, 409, 5xx or a network error. Lists what has to be set up again by hand (per-stage models, thinking settings, custom speech and transcription providers, AliDocMind's key pair, the VoxCPM backend, Baidu sub-sources) and that the per-browser capability switches do not carry over. - VoxCPM2 voices (page and READMEs): assign VoxCPM to tts, then Settings > Voice > VoxCPM voices, which appears only when tts resolves to voxcpm-tts. - Web search has no per-generation switch: it runs when the workspace's webSearch slot resolves, and turning it off affects later generations. - The READMEs' persistence section no longer says keys go in .env.local only. All six locales are updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * docs(config): startup errors name the field, or the line for broken YAML Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * refactor(settings)!: main's settings UI on the server-side model configuration (#1767) * feat(config): test a saved provider from the settings by its id The settings test buttons (and the model list fetch) name a provider the workspace has configured instead of sending its key and endpoint: the server resolves it from the deployment or the workspace's own configuration, under the same rules as a slot (the policy, self-hosted media presets, custom endpoints). - verify-model, verify-image-provider, verify-video-provider and verify-pdf-provider take `provider` (and `model`); probe-models takes `provider` for one of the workspace's own chat providers. - generate/tts and transcription take `previewProvider` (and `previewModel`) for a settings preview of a provider other than the slot's. - The settings view's catalogue models carry what the registry knows of them (capabilities, thinking controls, context window) and the registry entry that serves each capability, for the settings to show. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * refactor(settings)!: bring back the Token Plan, Model Services and Course Model sections on the server's configuration The settings dialog gets its earlier information architecture and layout back: Token Plan, Model Services (one tab per capability: language models, image, video, text to speech, speech recognition, document parsing, web search, each with its list of services and their panels), Course Model Config (the pipeline with a model per stage), Skills and General. The model map with its provider list and the separate Voice section are removed. The panels read and write the workspace's model configuration on the server instead of the browser store: - A service's key, endpoint (chat services only) and model list are its workspace provider (id = the service's preset id); keys are write-only (a mask is shown; replace or remove). A newly added service fills the root slots of what it serves that have nothing set yet. - Services the deployment configures are shown read-only; key-pair, self-hosted and (under a policy without workspace providers) all other services say that only the server's configuration can set them up. - The Course Model main model is the `llm` slot (with its thinking settings); each stage is its slot (follow the main model = no assignment of its own); the media switches set their root slot to null and restore what it held; locked slots are shown disabled. - Token Plan: connecting adds the plan's provider and fills the empty, unlocked slots it recommends; disconnecting removes it. - Test buttons name the saved service; no key leaves the browser. - The narration speed and test are back in the text-to-speech panel and the recognition language in the speech recognition panel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * feat(toolbar): the course model picker and extractor as before, on the server's slots The home toolbar's model picker gets its thinking control back (the `llm` slot's thinking settings) and its groups in the plan-first order the settings use, and the course material popover gets its extractor select back: it sets the `document` slot (a built-in parser that needs no key is added on first use). The set-up prompt opens Model Services. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * docs(settings): name the Token Plan, Model Services and Course Model sections The docs, READMEs and messages pointed at the Models section (model map, providers tab, "Connect a service") and the Voice section, which are gone. They now name the sections the settings have again: a plan's key in Token Plan, a service's key in Model Services, per-stage models and capability switches in Course Model Config, VoxCPM voices in Model Services → Text-to-Speech. The server-side explanations (locks, write-only keys, deployment-only services, the one-time import) stay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): web research dims only when search is off, as before Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): list a saved provider's models without naming a model Fetching the models of a saved chat provider passed its bare id to the chat resolver, which requires `provider:model`, so every request was refused. The provider's connection is now resolved without a model (still only the workspace's own providers, and the endpoint is still checked). The settings view test also covers an OpenAI-compatible deployment provider's listed chat models. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * feat(settings)!: Course Model Config is the course model map Course Model Config shows the course model map again in place of the earlier pipeline panel: the default model card on top, the pipeline as a two-row serpentine with the content station expanding to its page types, editing on the card (follow the parent, a model or off, and a fallback for chat slots), "Set by the server" locks, solid and dashed lines, zoom, pan and keyboard. Its provider tab and first-run card stay out: services are set up in Model Services and Token Plan, and the map links to Model Services where it needs one. - One switch implementation (flipSwitch): off sets the slot to null, on restores what it held, else takes the first service that serves it (speech input returns to the browser's recognition), else opens the picker. What it turned off is remembered, and what it restored forgotten, only once the server confirmed the change. - Speech input shows its switch while it runs in the browser (unset), and a service can be picked for it then. - The server's providers count wherever chat models are offered (the map, the toolbar, Model Services); the map's "no model" note shows only when nothing at all offers one. - Model Services names a provider by its preset and id ("OpenAI-compatible · gateway", with a generic logo), and shows one named after a built-in service as that service's entry. - A typed key stays in the field when the server refuses it; it is cleared once saved (Doubao's paired fields too). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): provider logos in the model pickers; thinking, keyless services and model discovery on the map Logos: the home toolbar's model picker shows the provider's logo again, on the pill and on every group and model, as it did before the settings restructure; so do the course model map's card picker and its read-only pill. A provider looks the same everywhere: its plan's or built-in service's logo (a provider named after one included), its preset's, or the generic service icon for a custom endpoint, as in Model Services. The naming and logo helpers move to a data-only module the toolbar can load. Review fixes: - The map's card picker sets the thinking settings of a chat slot's own model (the main model and each stage), against the view it shows; locked slots have no picker. - A service that needs no key (the browser's own speech) is offered in a media slot's picker, and the text-to-speech panel can make it the narration: the provider is added, then the slot assigned against the view the add answered. - Fetching a provider's models reports them as added only once the server saved the list; a refused or lost write leaves a failure to retry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): removing a provider's key frees the slots that needed it Removing the stored key of a workspace provider left the slots assigned to it pointing at a provider that can no longer be called, so the default model (or a stage) failed at generation time while the settings still showed it in use. Removing the key now drops those assignments, as removing the provider does, and the slots follow their parents again. Only the capabilities whose provider needs a key are affected: a local model server or a keyless search keeps what it serves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(config): connecting a token plan fills an empty web search slot A token plan's web search has no models to pick, so the plan's preset recommended nothing for the webSearch slot and connecting a plan (TokenDance, MiniMax) left search unassigned, where the browser-side Token Plan used to select the plan's search. The preset now recommends the plan's search for it, and the first-run fill assigns the provider by itself to the slot when it is still empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): show each TTS service's full request URL The server-backed TTS panel showed only the preset base URL as the request URL. Append the path each built-in service calls, as main did, so Gemini shows /interactions and Doubao /unidirectional. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * ci: stop triggering on integration/provider-config before it merges to main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(settings): cache provider logos so switching service tabs does not blank them (#1777) Next serves public/ files with `Cache-Control: public, max-age=0`, so each logo a newly mounted list draws is revalidated before it paints. Switching tabs in Model Services mounts a fresh list, and its logos stayed blank until every revalidation came back. Cache /logos/* for a day with a week of stale-while-revalidate; the files are not content-hashed, so not immutable. Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): hide the Skills section when the agent runtime is unavailable (#1778) Without the agent runtime, GET /api/agent/skills returns 404 by design, so the Settings Skills section could only ever show a load error with a retry that cannot succeed. Probe GET /api/agent/runtime once per tab and list the Skills item only when it reports `enabled` (the flag plus DATABASE_URL, the same check the skills route gates on). The item stays hidden while the answer is unknown, and a request to open the dialog on `skills` without the runtime lands on the first section. Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(generation): show the selected extractor's formats in the material upload hint (#1779) The attach popover's dropzone hint claimed documents, slides, spreadsheets and images for every extractor, so with unpdf (PDF only, plus the built-in plain-text extractor) users could pick a .docx via "All Files" and only then hit a generic "unsupported" error. - The hint now lists exactly the formats the active extractors accept (getFormatLabelsForProviders), with the per-file size limit. - The unsupported-file error names the extractor and its supported formats; it covers the file picker, drag-and-drop onto the dropzone, and the cleanup that drops attached files after switching extractors. - Format labels are file-type names shared by all locales; the list separator is localized. Strings updated in all 12 locales. Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(settings): connecting a token plan applies its recommended configuration (#1780) Since model configuration moved to the server, connecting a token plan only filled the slots that were still empty, so a workspace that had already picked models never switched to the plan's setup. The browser-side Token Plan used to make the plan's default model, its recommended course stages and its image, video, speech and search services the active selection. Connecting a plan (or saving a new key for a connected one) now applies the plan's recommendation as one slots change: the default model, each course stage the plan names, and its media and search services. When that would replace assignments the workspace made itself, the panel asks first: use the plan's recommended setup, or keep the current one and fill only the empty slots. Slots the deployment locks are never offered or changed, and a plan yields the slots a connected plan of higher priority recommends, whatever the connect order. A replaced language-model assignment keeps its fallback; thinking settings go with the model they were set for. Disconnecting still removes the provider, which frees the slots that named it. Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(home): the toolbar picker sets the default model and shows stages set separately (#1781) * feat(home): label the toolbar model as the default and show stages set separately The home toolbar picker only sets the `llm` root, but read as if it chose the model for everything. It now says what it changes: - the pill reads "Default · <model>" (the prefix is hidden on phones); - a hint after it counts the stages under `llm` whose own setting resolves to something other than the default (inheriting or matching stages do not count; deployment-set stages do), lists them in a tooltip with the map's stage names, and opens Course Model Config on a click; - the dropdown says picking here leaves stages set separately alone; - when every stage a course is always generated with (outline, each page type's content, actions) has its own setting, the pill summarises the per-stage setup (naming the Token Plan when one plan provider serves them all) and opens Course Model Config instead of offering a switch. Picking a model still changes only `llm`; the locked and first-run states are unchanged. New strings are added to all 12 locales. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(logos): size the DeepSeek logo to its own view box The SVG declared width 182 and height 29 around a 34x29 view box, so any square icon slot scaled it as a wide strip and the whale drew as a dot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent): don't inherit a thinking effort the agent can't use; recover sessions after a failed first run (#1782) * fix(agent-runtime): drop the thinking effort the agent slot inherits The agent slot follows llm, so a thinking level picked for the default model (the home toolbar writes it on llm) reached the agent driver, which refuses any thinking effort because its tool calls cannot carry one. Every agent run failed for a user who had picked a thinking level. The agent slot now declares that it carries no thinking effort: - Resolution drops an effort the agent inherits from an ancestor and keeps the rest of the thinking settings; an inherited effort of none stays "thinking off". - An effort set on the agent slot itself is refused when it is saved: openmaic.yml at load and a workspace change through the settings API. The driver's own check stays as a backstop for settings stored earlier. - The model map's thinking control for the Agent card offers no effort levels: an effort model that can be switched off shows on/off, and one that cannot shows nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(agent-runtime): keep a session usable after a run that failed while starting A run that fails before it completes any message (for example, while resolving its model) writes nothing to the entry tree, but its lifecycle frames are in the event log. The runner treated any lifecycle frame as proof that the tree should hold history, so every later run of that conversation failed with "tree is empty after a prior run". The empty-tree check now asks the event log whether a prior run completed a message. The runner appends every completed message to the tree right after its message_end event, so an empty tree is refused only when a message_end exists: the tree lost history it held. An empty tree after runs that never completed anything is legal, and the next run starts the conversation over: - it resumes the conversation instead of opening it again, so the opening prompt is not painted twice; - a durable message is taken as the session's opening message only when it was posted before the first run; a message posted after a failed start is delivered as a follow-up, not consumed in place of the session's prompt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(config): workspace-provider policy covers request headers; startup secret warnings; upgrade notes (#1783) * fix(model-config): ignore request-named providers when workspace providers are off With `policy.allowWorkspaceProviders: false`, users may only use the providers openmaic.yml declares. The deprecated request paths still let a request run on its own model, key and endpoint while a slot was unassigned. Under that policy the language model and media resolvers now skip the provider a request names, document extraction keeps only a self-contained extractor from the request fields, and the header form of the provider test routes (verify-model, verify-image-provider, verify-video-provider) is refused. Behaviour is unchanged when the policy is true or unset. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * test(model-config): pin the media defaults an upgrade keeps Before the server-side settings, the browser switched image, video and narration on at its first sync with the server whenever the server had a provider for them, and the classroom chat and the agent searched the web whenever a search provider was configured. The translated legacy defaults keep those capabilities on, unlocked, so a workspace can switch each off; an explicit openmaic.yml assignment stays as written and locked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * feat(secrets): warn at startup when stored keys will not open Keys saved in the model settings are sealed under OPENMAIC_SECRET_KEY, or under a secret generated in data/instance-secret.key when it is unset. On a host whose data directory does not survive a restart, or with several replicas, a new secret is generated and the stored keys stop opening; on a read-only data directory no secret can be created and saving a key fails. At boot, with one query in the background, the server now warns when: - OPENMAIC_SECRET_KEY is unset and the data directory cannot hold a new secret file; - the secret file is missing (so a new one would be generated) while the database holds keys sealed under an earlier secret; - stored keys were sealed under a different secret than the current one. Each warning says the keys have to be entered again and recommends setting OPENMAIC_SECRET_KEY. The server still starts in every case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * docs: upgrade notes for server-side model configuration Add breaking changes and upgrade notes to CHANGELOG.md for the move of model configuration to the server: openmaic.yml and capability slots, MODEL_ROUTES refusing to start without openmaic.yml (with the route-to-slot mapping, including maic-agent-driver -> agent), the narrowed /api/generate-classroom body, deprecated request fields and the workspace-provider policy, the media capability defaults after an upgrade, the one-time browser settings import and what it cannot carry, and OPENMAIC_SECRET_KEY persistence across restarts and replicas. The configuration and deployment docs (all languages) and .env.example now cover replicas, ephemeral and read-only data directories, the new startup warnings, and request fields being ignored under the policy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(import): keep settings the server could not take; Azure regional endpoints; keyless compatible providers (#1784) * fix(model-config): keep what the browser import could not move, and accept Azure Speech regional endpoints The one-time import of browser model settings deleted the staged proposal, keys included, on any 2xx answer, even when the server skipped items. The version 5 store migration had already dropped the originals, so a skipped provider lost its key everywhere. An Azure TTS/STT user with a regional endpoint lost theirs this way: the server refused the endpoint as a custom media endpoint. Import: - Only what the answer shows the workspace holding leaves the browser. Every other item (skipped as invalid, an id the deployment declares, or unconfirmed because the answer could not be read) is kept in the browser as staged, with its keys, under its own key. Skips that leave nothing behind (EXISTS, a locked slot) are not kept. If the items cannot be kept, the proposal stays for a later load. - What the builder cannot propose is kept the same way at migration time: custom speech/transcription providers, AliDocMind's key pair, custom chat providers without an endpoint or of an unsupported type. - Kept items are never sent again and do not re-trigger the import. A toast tells the user once; Settings -> Model Services lists them with the reason and a copy-key button until they are discarded. Setting one up again (a new workspace provider of its preset, or the slot) clears it. Clearing the local cache keeps them. - The import answer now carries a `code` per skipped item, and a provider id the deployment declares is reported as PROVIDER_RESERVED. Azure Speech: - Workspace azure-tts / azure-asr providers take their official regional endpoint (https://<region>.tts.speech.microsoft.com, https://<region>.api.cognitive.microsoft.com or .stt.speech.microsoft.com), region [a-z0-9]+, https only, no userinfo, port, path, query or fragment. It is checked at save time and at resolution (resolve-slot marks anything else as a custom endpoint, which media resolution refuses), and stored normalised. Their registry default only names the region as a placeholder, so they now require it. The TTS/ASR panels get a regional endpoint field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(model-config): let a keyless OpenAI-compatible provider run A self-hosted OpenAI-compatible server (Ollama, vLLM) declared in openmaic.yml as `preset: openai-compatible` without `apiKey` resolved, but building its model threw "API key required for provider: openai": the preset rides on the OpenAI registry entry, which requires a key. - The openai-compatible preset marks its key optional, and the slot model passes that to getModel (a new `requiresApiKey` override on ModelConfig). Every other preset keeps the registry's rule, so OpenAI itself still needs a key and fails with the same clear error. - A request without a key no longer carries an empty `Authorization: Bearer ` header; with a key it is sent as before. On main, a custom OpenAI-compatible provider sent from the browser was built the same way (providerType openai, registry default requiresApiKey true) and also needed a key on the server; keyless worked only for registry entries marked keyless (Ollama, Lemonade). This lets the documented keyless openmaic.yml provider work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(model-config): carry narration, image and video switched off over to the workspace The browser import carried a capability's "on" choice but not an explicit "off" for narration, images and video. A user who had switched one off got it back on after upgrading, because the deployment's defaults now turn those capabilities on. When the old store has `ttsEnabled`, `imageGenerationEnabled` or `videoGenerationEnabled` stored as false while a usable provider for the capability was there, the import proposes `tts: null` / `image: null` / `video: null`, as `asrEnabled: false` already becomes `asr: null`. Earlier builds defaulted these switches to off and turned them on by themselves once a provider was usable (a server provider on the first load, a key the user entered), and off when none was. So `false` with a usable provider (server-configured and not switched off by the operator, or with the user's own key; browser speech synthesis does not count) is the user's choice, and `false` without one is only the default, which is not carried over. A server that gained a provider after the browser's first load cannot be told apart and is read as off: that is visible in the settings and costs nothing, while reading it as on could start paid generation the user refused. Web search off is not carried over: it only stopped course research, while chat and the agent kept searching through the same provider. A slot the deployment locks is skipped like any other import (SLOT_LOCKED). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(import): drop a browser-kept key only when the server holds the same one (#1785) * fix(import): drop a browser-kept setting only when the server holds the same one The one-time import of browser model settings could still lose a key: - A provider id the workspace already had was answered `EXISTS`, and the browser deleted its copy, even when the workspace held another key. The server now compares the proposed provider with the stored one (preset, endpoint in its normalised form, models, and the key, decrypted and compared in constant time) and answers `EXISTS_SAME` or `EXISTS_DIFFERENT`. Only `EXISTS_SAME` lets the browser drop its copy; a key this instance cannot open is never confirmed. Nothing else about the stored provider is returned. - Provider ids and slot ids shared one namespace in the answer, so a slot `tts` that was imported confirmed a refused provider `tts` and its key was deleted. The answer now names each item's kind (`{ kind: 'provider' | 'slot', id }`), and kept items are keyed by kind and id. - The notice in Settings cleared a kept provider, key included, as soon as any new workspace provider of its preset appeared, even one without a key. A kept key now leaves only when such a provider holds it as far as the view shows (key set, readable, same mask); otherwise it stays, with its copy button, until the user discards it. Key pairs and keys too short to show in a mask are never cleared this way. The key mask moves to a module the server and the browser share. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * docs(changelog): the narration, image and video off switches are carried over The changelog still said the image, video and narration off switches were not carried over. They become `tts`/`image`/`video: null` when a usable provider was set up, as the configuration docs and the import README already say; only the research switch, and a switch off only by default, are not carried over. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR * fix(import): never clear a kept key by itself A kept provider that holds a key (or a key pair) used to leave the browser once a new workspace provider of its preset held a key whose mask matched. A last-four-characters match does not confirm the workspace holds that key, so such an item now stays, with its copy button, until the user discards it. Kept items without a key still leave by themselves once the view shows them set up again. The key mask goes back to the server module, its only user. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(import): keep browser settings on a refused or self-contradicting answer (#1787) Two more ways the one-time import of browser model settings could lose a key: - A 400 removed the proposal outright. The server refuses the whole proposal then (an unexpected top-level field is enough), so sending it again cannot succeed, but its keys existed nowhere else. Every item is now kept in the browser first, keys included, as refused with the server's message, and only then is the proposal removed; if they cannot be kept, the proposal stays. Kept items are never sent again. 401, 404, 409, 5xx and network failures still keep the proposal for a later load. - An answer that listed an item as both imported and skipped, or skipped it with different codes, let `imported` (or the last code) win, which could delete a key the server did not hold. Such an item is now kept as unconfirmed. Claude-Session: https://claude.ai/code/session_01EF6G824rLqUXidRrqrMskR Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> | 15 天前 | |
Initial commit: OpenMAIC AI classroom platform Next.js 16 App Router application with: - AI-powered interactive classroom generation from PDF/text requirements - Multi-agent discussion system (teacher, students, assistant roles) - Provider abstraction for LLM, TTS, ASR, image, video, web search - Local-first data architecture (IndexedDB/Dexie) - i18n support (zh-CN/en-US) - Structured logger, SSRF guard, lint/build passing (0 errors) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> | 7 个月前 | |
feat: add ElevenLabs TTS provider (#134) * feat: add ElevenLabs TTS provider * fix(tts): wire ElevenLabs provider config Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> --------- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn> | 6 个月前 | |
feat(asr): add local FunASR provider (#1044) Signed-off-by: LauraGPT <199134975+LauraGPT@users.noreply.github.com> Co-authored-by: LauraGPT <199134975+LauraGPT@users.noreply.github.com> | 2 个月前 | |
Initial commit: OpenMAIC AI classroom platform Next.js 16 App Router application with: - AI-powered interactive classroom generation from PDF/text requirements - Multi-agent discussion system (teacher, students, assistant roles) - Provider abstraction for LLM, TTS, ASR, image, video, web search - Local-first data architecture (IndexedDB/Dexie) - i18n support (zh-CN/en-US) - Structured logger, SSRF guard, lint/build passing (0 errors) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> | 7 个月前 | |
Initial commit: OpenMAIC AI classroom platform Next.js 16 App Router application with: - AI-powered interactive classroom generation from PDF/text requirements - Multi-agent discussion system (teacher, students, assistant roles) - Provider abstraction for LLM, TTS, ASR, image, video, web search - Local-first data architecture (IndexedDB/Dexie) - i18n support (zh-CN/en-US) - Structured logger, SSRF guard, lint/build passing (0 errors) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> | 7 个月前 | |
feat: add Grok (xAI) support as an LLM, image generation, and video generation provider (#113) * feat: add Grok (xAI) provider support for LLM, Image and Video generation - Add Grok as LLM provider (OpenAI-compatible, 11 models including grok-4.20, grok-4, grok-3) - Add Grok Image generation provider (grok-imagine-image, grok-imagine-image-pro) - Add Grok Video generation provider with async polling (grok-imagine-video) - Add env vars: GROK_*, IMAGE_GROK_*, VIDEO_GROK_* - Add web search note: Grok search is via chat completions, not standalone API - Update README/README-zh with Grok in supported providers Closes #87 * fix: ensure new image/video providers are auto-added to persisted settings Add ensureBuiltInImageProviders and ensureBuiltInVideoProviders functions that run on every store rehydrate, mirroring the existing LLM provider sync logic. Without this, Grok Image/Video providers would not appear for users with existing localStorage settings. * fix: add missing English translation for providerGrokVideo * style: fix prettier formatting for grok video provider * fix: remove unsupported grok multi-agent model and improve video error | 6 个月前 | |
support recent model providers (#481) | 5 个月前 | |
Initial commit: OpenMAIC AI classroom platform Next.js 16 App Router application with: - AI-powered interactive classroom generation from PDF/text requirements - Multi-agent discussion system (teacher, students, assistant roles) - Provider abstraction for LLM, TTS, ASR, image, video, web search - Local-first data architecture (IndexedDB/Dexie) - i18n support (zh-CN/en-US) - Structured logger, SSRF guard, lint/build passing (0 errors) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> | 7 个月前 | |
Initial commit: OpenMAIC AI classroom platform Next.js 16 App Router application with: - AI-powered interactive classroom generation from PDF/text requirements - Multi-agent discussion system (teacher, students, assistant roles) - Provider abstraction for LLM, TTS, ASR, image, video, web search - Local-first data architecture (IndexedDB/Dexie) - i18n support (zh-CN/en-US) - Structured logger, SSRF guard, lint/build passing (0 errors) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> | 7 个月前 | |
feat: add lemonade to provider (#508) * feat: add lemonade to provider * fix(lemonade): correct TTS/ASR model IDs and expand Kokoro voices - TTS model: kokoro → kokoro-v1 - ASR model: single 'whisper' → 6 Whisper variants (Base/Large-v3/Large-v3-Turbo/Medium/Small/Tiny), default Whisper-Base - Expand lemonade-tts voices to full Kokoro v1 set (54 voices across en-US/en-GB/zh-CN/ja-JP/es/fr-FR/hi/it-IT/pt-BR) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(lemonade): zh-TW i18n parity, real Kokoro default voice, language code consistency - zh-TW.json: add missing providerLemonadeTTS/ASR/Image keys - TTS default voice: 'default' → 'af_heart' (Kokoro expects real voice ID, not placeholder) - Voice language codes: 'es'/'hi' → 'es-ES'/'hi-IN' for consistency Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(lemonade): add unit tests for image/TTS/ASR adapters and wav-utils Covers: - lib/media/adapters/lemonade-image-adapter: request shape, base URL fallback, custom model, optional Bearer auth, error paths, connectivity probe - lib/audio/tts-providers (lemonade-tts case): request body (model/voice/format), af_heart fallback, optional auth, error path - lib/audio/asr-providers (lemonade-asr case): WAV-only validation, model/language form fields, "audio is empty" graceful empty result, error propagation, default model fallback - lib/audio/wav-utils: isWavBlob detection by MIME and filename, normalizeASRUploadAudio pass-through and wav handling Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(lemonade): remove text path fork and harden fallbacks * style: format lemonade tts files * i18n: add lemonade voice language labels * fix(lemonade): align image default model and harden wav detection * test(lemonade): fix File BlobPart typing * fix(lemonade): simplify tts voice selection and clear asr test state * docs: highlight Lemonade support --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: wyuc <wang-yc24@mails.tsinghua.edu.cn> | 5 个月前 | |
Initial commit: OpenMAIC AI classroom platform Next.js 16 App Router application with: - AI-powered interactive classroom generation from PDF/text requirements - Multi-agent discussion system (teacher, students, assistant roles) - Provider abstraction for LLM, TTS, ASR, image, video, web search - Local-first data architecture (IndexedDB/Dexie) - i18n support (zh-CN/en-US) - Structured logger, SSRF guard, lint/build passing (0 errors) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> | 7 个月前 | |
Initial commit: OpenMAIC AI classroom platform Next.js 16 App Router application with: - AI-powered interactive classroom generation from PDF/text requirements - Multi-agent discussion system (teacher, students, assistant roles) - Provider abstraction for LLM, TTS, ASR, image, video, web search - Local-first data architecture (IndexedDB/Dexie) - i18n support (zh-CN/en-US) - Structured logger, SSRF guard, lint/build passing (0 errors) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> | 7 个月前 | |
chore: update Ollama logo to official version (#400) * chore: update Ollama logo to official version Closes #399 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: improve OpenAI and Ollama logo visibility across themes OpenAI logo (white fill) was invisible on light backgrounds, and Ollama logo (black fill) was invisible on dark backgrounds. Normalize OpenAI SVG to black fill and apply dark:invert via a central MONO_LOGO_PROVIDERS set. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> | 6 个月前 | |
chore: update Ollama logo to official version (#400) * chore: update Ollama logo to official version Closes #399 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: improve OpenAI and Ollama logo visibility across themes OpenAI logo (white fill) was invisible on light backgrounds, and Ollama logo (black fill) was invisible on dark backgrounds. Normalize OpenAI SVG to black fill and apply dark:invert via a central MONO_LOGO_PROVIDERS set. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> | 6 个月前 | |
support recent model providers (#481) | 5 个月前 | |
Initial commit: OpenMAIC AI classroom platform Next.js 16 App Router application with: - AI-powered interactive classroom generation from PDF/text requirements - Multi-agent discussion system (teacher, students, assistant roles) - Provider abstraction for LLM, TTS, ASR, image, video, web search - Local-first data architecture (IndexedDB/Dexie) - i18n support (zh-CN/en-US) - Structured logger, SSRF guard, lint/build passing (0 errors) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> | 7 个月前 | |
Initial commit: OpenMAIC AI classroom platform Next.js 16 App Router application with: - AI-powered interactive classroom generation from PDF/text requirements - Multi-agent discussion system (teacher, students, assistant roles) - Provider abstraction for LLM, TTS, ASR, image, video, web search - Local-first data architecture (IndexedDB/Dexie) - i18n support (zh-CN/en-US) - Structured logger, SSRF guard, lint/build passing (0 errors) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> | 7 个月前 | |
[codex] Add Bocha web search provider (#524) * Add Bocha web search provider * Address Bocha web search review feedback | 5 个月前 | |
feat(token-plan): add TokenDance one-key preset for every modality (#1525) * feat(token-plan): add TokenDance one-key preset for every modality TokenDance is a model gateway: chat and images are OpenAI-compatible at /gateway/v1, and the same key authenticates vendor-protocol routes on the same host (Ark, MiniMax, Bocha). The preset reuses the existing adapters with those route prefixes as base URLs, so one key lights up LLM, image, video, TTS and web search from Settings -> Token Plan. - providers: add a built-in `tokendance` OpenAI-compatible provider (TOKENDANCE_* env prefix, logo, provider name in all locales) - token-plan: add the TokenDance preset (Seedream image, MiniMax H3 video, MiniMax speech TTS, Bocha web search) - seedream: use a base URL that already ends in a version segment verbatim, so gateway routes like `/ark/v3` do not get `/api/v3` appended - minimax-video: route H3-family models through the v2 task API (content array submit, task-envelope poll); connectivity checks for H3 probe auth on the v2 query route instead of submitting a billable task - README: add a one-key quick example and replace the Gemini-specific model recommendation with a provider-agnostic setup recommendation Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qrrq9CPwb718mpouz8Y2KL * fix(token-plan): accept preset web-search base URLs and report H3 dimensions per ratio - web-search: the client base URL allowlist also accepts the exact base URL a built-in token plan preset writes for that provider, derived from TOKEN_PLAN_PRESETS. Applying a plan whose web-search route is not an official vendor host previously stored a URL that the route rejected with 400. Any other client URL is still rejected. - minimax-video: report H3 v2 clip dimensions for 16:9, 9:16, 4:3 and 1:1 instead of assuming landscape for every non-portrait ratio. - tests: pin the allowlist for every preset, the 1:1 H3 dimensions, and clear TOKENDANCE_* in the provider-config env isolation list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qrrq9CPwb718mpouz8Y2KL --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> | 1 个月前 | |
Initial commit: OpenMAIC AI classroom platform Next.js 16 App Router application with: - AI-powered interactive classroom generation from PDF/text requirements - Multi-agent discussion system (teacher, students, assistant roles) - Provider abstraction for LLM, TTS, ASR, image, video, web search - Local-first data architecture (IndexedDB/Dexie) - i18n support (zh-CN/en-US) - Structured logger, SSRF guard, lint/build passing (0 errors) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> | 7 个月前 | |
feat(token-plan): one-click token-plan setup + deployment usage dashboard (#784) * feat(usage): add usage normalization, pricing, model-fetch, balance, storage Foundational layer for token-plan usage tracking (cc-switch-modeled): - lib/usage/normalize.ts: AI SDK v6 usage → four-class token shape - lib/usage/pricing.ts + defaults: per-class USD pricing table - lib/server/model-fetch.ts: /models candidate-URL multi-fallback (ported) - lib/usage/balance-providers.ts: built-in balance queries + detection - lib/server/usage-storage.ts: fire-and-forget jsonl logging to data/usage All pure/storage logic covered by vitest (32 tests). * feat(usage): capture token usage at the callLLM/streamLLM chokepoint - callLLM records result.usage before returning - streamLLM wraps onFinish to record totalUsage on stream completion, preserving any caller-supplied onFinish - provider/model derived from the model instance (no route changes) - fs-backed storage imported dynamically; fire-and-forget, never throws - include_usage is already sent by @ai-sdk/openai, so streaming is covered * feat(usage): add probe-models, balance, and usage API routes - POST /api/provider/probe-models: discover chat models via /models with candidate fallback; SSRF-guarded; filters non-chat ids; 401/404 typed - POST /api/provider/balance: built-in balance detection + billing fallback - GET /api/usage: aggregate jsonl by model/day/source with costIncomplete flag Verified e2e against the live MAIC gateway: 16 chat models (6 filtered), balance detected, and a real callLLM writes a costed usage row. * feat(settings): add token-plan preset picker to provider dialog - lib/config/token-plan-presets.ts: data-driven vendor presets (Huawei/MiniMax/ Xiaomi token plans, OpenRouter/SiliconFlow gateways, DeepSeek/GLM/Qwen/Hunyuan/ Doubao direct) with baseURL, protocol, optional modelsUrl, category - add-provider-dialog: '选择厂商/自定义' tabs; picking a preset auto-fills baseURL+protocol+modelsUrl, custom tab unchanged - ProviderSettings.modelsUrl carries the optional /models override - i18n keys added across all 8 locales Verified in-browser: preset picker renders grouped by category. * feat(settings): add Fetch Models button and balance bar to provider panel - 拉取模型: probes /models, merges discovered ids into the model list (dedupe, keeps manual additions), with success/no-endpoint/auth messages - 查询余额: queries /api/provider/balance, renders a balance bar or a 'check console' hint when unsupported - index.tsx: handleModelsFetched merges probe results into provider config - i18n keys across all 8 locales; removed a now-unused eslint-disable Verified in-browser against MAIC gateway: 16 models fetched, balance shown. * feat(settings): add usage dashboard to System Settings - usage-dashboard.tsx: echarts dual-axis daily trend (tokens + cost), totals cards, by-model table, refresh; reads GET /api/usage - mounted at the top of GeneralSettings (系统设置) - honest disclaimer + costIncomplete marker when a model lacks pricing - i18n keys across all 8 locales Verified in-browser: shows 1 request / 31 tokens / $0.0003 from a prior call. * feat(token-plan): multi-modal one-click setup in System Settings - token-plan-presets.ts: presets now declare per-modality targets (llm/image/video/tts/webSearch); MiniMax is the full-set template, others LLM-only — extend by adding entries (at-our-best adaptation) - apply-token-plan.ts: fills one key into every declared modality via injected store setters, isolating per-modality failures (TDD, 4 tests) - token-plan-settings.tsx: new sidebar page — pick plan, enter key, one-click apply lights up adapted modalities (+LLM model probe), shows 'not adapted yet' for the rest, balance bar reused - reverted add-provider dialog to plain custom form (preset picker moved here) - i18n across all 8 locales Verified in-browser: Token Plan page renders, MiniMax shows all 5 modalities, apply/balance UI wired. 859 tests pass, build clean. * feat(token-plan): add custom token plan entry to Token Plan page - Custom card at the bottom of the provider list: pick it to manually enter name + protocol + baseURL (LLM-only), then key + one-click apply via the same applyTokenPlan flow (mirrors cc-switch's custom provider) - effectivePreset unifies preset vs custom for apply/balance/probe - i18n keys (customGroup/customName/customHint) across all 8 locales Verified in-browser: custom card expands the manual form; preset flow intact. * feat(token-plan): reflect persisted config on the Token Plan page Other settings panels read the store directly, so they survive section switches; the Token Plan page only wrote the store, so it looked blank on return. Now it also reads providersConfig: - selecting a preset prefills its saved API key - configured presets show a '已配置/Configured' badge State persistence was never broken (apply writes zustand→localStorage); this fixes the missing read-back so the page reflects it. * fix(settings): isLLMProviderConfigured crashed on providers without models Root cause of 'token plan config disappears': applyTokenPlan writes a new LLM provider with no models yet (probe fills them later). isLLMProviderConfigured did config.models.length unguarded → threw inside setProviderConfig's resolver → the whole set() aborted → key/baseUrl/type never persisted. - guard models in isLLMProviderConfigured (shared validator; affects any provider written without a models array) - seed models:[] in applyTokenPlan's LLM write for a valid initial shape - regression tests: validator no longer throws; apply+probe write keeps apiKey Verified in-browser: DeepSeek persists key and shows 已配置 after section switch. * feat(token-plan): add remove/teardown for a configured token plan Apply had no inverse. Add removeTokenPlan: clears the API key and disables (enabled:false) every modality the plan declared; a custom LLM provider is deleted entirely (removeProvider), built-ins keep the cleared shape. - removeTokenPlan in apply-token-plan.ts (mirrors applyTokenPlan, isolated per-modality, injected setters; 3 tests) - trash button on configured preset cards in the Token Plan page; resets page state if the removed plan was selected - i18n 'remove' across all 8 locales Verified in-browser: removing DeepSeek empties its key and drops the 已配置 badge. * feat(usage): track multimodal usage, drop cost/pricing Reframe usage stats as pure usage (no cost), per user decision: - usage-storage: drop all cost fields; add kind (llm/image/video/tts/asr) + quantity + unit; LLM keeps token counts, others store quantity - delete lib/usage/pricing.ts + pricing-defaults.json + its test - instrument image/video/tts at the server-only API routes (not in the provider dispatch — those files are in the client graph and importing fs-backed usage-storage broke the client bundle) - /api/usage: aggregate by model/day/modality, no cost - dashboard: per-modality usage (tokens/images/seconds/chars), token-only daily trend, no '$'; i18n cost keys replaced with modality/unit keys - backward compatible: legacy rows (no kind, stray cost fields) read as llm 912 tests pass, build clean. * feat(usage): per-modality dashboard layout + softer dark-mode chart - group usage into per-modality sections (LLM/image/video/tts/asr), each table's usage column uses one consistent unit (token/image/sec/char) — no more mixed units in a single column - summary chips per modality with their own unit - trend chart now plots daily REQUESTS (unit-agnostic, works for any modality) instead of LLM-only tokens - theme-aware chart: faint thin line + soft gradient area, muted axis/grid colors via useTheme — fixes the harsh solid stroke in dark mode Verified in-browser (dark): TTS shows '字符', LLM shows 'Token', separate sections; chart no longer has a hard line. * refactor(usage): dedupe model/fetch/usage helpers, parallel balance probe Review cleanups on the token-plan/usage branch: - extract modelInfoFromId() (shared vision heuristic + ModelInfo shape) - extract fetchWithTimeout() shared by model-fetch and balance-providers - extract recordGenerationUsage() to dedupe the image/tts/video routes - queryBalance: fetch billing subscription + usage in parallel - parseOneApiBilling: report quota without remaining when usage endpoint is unavailable, instead of implying zero spend (full balance) - split the two jammed imports in settings/index.tsx * feat(token-plan): drop custom token-plan support Custom token plans (manual baseURL/protocol entry) added complexity for little gain — a one-off provider is better configured directly on the Providers page. Token Plan is now preset-only: - remove custom mode, manual fields, and the custom card from the UI - collapse effectivePreset back to the selected preset - drop the now-dead removeProvider action + custom-id branch in removeTokenPlan - remove orphaned customGroup/customName/customHint i18n keys (8 locales) * feat(token-plan): add Volcengine/Tencent/Bailian plans, drop balance feature Add three vendor token-plan presets (all map to existing built-in LLM providers, so it's data-only — no new adapters): - 火山方舟 Volcengine Ark → doubao, OpenAI /api/v3 - 腾讯 TokenHub Token Plan → tencent-hunyuan, OpenAI /plan/v3 (the plan-specific base; /v1 is the pay-as-you-go gateway) - 阿里百炼 Token Plan → qwen, cross-model plan (Qwen + DeepSeek/Kimi/ GLM/MiniMax) on one key; model list is probed/entered Remove the balance/quota feature entirely — we now track usage, not cost, and every vendor's balance query needs its own cloud AK/SK + signature (Volcengine SigV4 / Tencent TC3 / Aliyun BSS), which the Bearer-key billing-endpoint probe never supported anyway: - delete lib/usage/balance-providers.ts, /api/provider/balance, its test - strip the Check Balance button + balance bar from the token-plan page and the provider config panel - remove the 4 balance i18n keys across all 8 locales - restore an eslint-disable the branch had dropped in provider-config-panel * feat(token-plan): use cloud-brand logos for vendor token plans The three vendor plans are cloud offerings, not single-model products, so icon them with the cloud brand rather than a model logo: - 火山方舟 → volcengine.svg (was doubao.svg) - 腾讯 TokenHub → tencentcloud.svg (was hunyuan.svg) - 阿里百炼 → alibabacloud.svg (was bailian.svg) Logos are the colored brand variants from lobehub/lobe-icons, matching the existing colored-logo style (plain <img>, no dark:invert needed). * feat(token-plan): keep only MiniMax and Volcengine presets Trim the token-plan list to the two we want to ship: MiniMax (full-set template) and 火山方舟 Volcengine Ark. Drop the Tencent/Bailian plans and the OpenRouter/SiliconFlow/DeepSeek/GLM/Qwen entries. - remove the now-unused tencentcloud.svg / alibabacloud.svg logos (volcengine.svg stays; the other logos are still used by the provider registry) - retarget the LLM-only apply test from the deleted deepseek preset to volcengine-ark * feat(token-plan): restore aggregator/third-party presets Previous commit over-trimmed: the intent was to drop only the Tencent and Bailian token plans, not the OpenRouter/SiliconFlow/DeepSeek/GLM/Qwen entries. Bring those back; keep only Tencent/Bailian removed. - token_plan: MiniMax, 火山方舟 Volcengine Ark - aggregator: OpenRouter, SiliconFlow - third_party: DeepSeek, GLM, Qwen Revert the apply test back to the deepseek fixture (restored). tencentcloud.svg / alibabacloud.svg stay deleted (their plans are gone). * fix(token-plan): point Volcengine plan at the Coding Plan endpoint The plan's ark--prefixed API keys authenticate only against /api/coding/v3, not the general /api/v3 endpoint — the latter rejects them with "The API key format is incorrect", so model probing returned nothing. Switch the base URL to https://ark.cn-beijing.volces.com/api/coding/v3. * fix(token-plan): Volcengine is an Agent Plan (Anthropic /api/plan) Per the Ark Agent Plan docs, the ark--prefixed keys authenticate ONLY against the dedicated Anthropic-compatible base https://ark.cn-beijing. volces.com/api/plan ("其他 Base URL 无法在 Agent Plan 中使用"). The general /api/v3 and the Coding Plan /api/coding endpoints both reject the key as "API key format is incorrect", which is why model probing kept returning 0. - baseUrl → https://ark.cn-beijing.volces.com/api/plan/v1 (the /v1 lets the Anthropic SDK land on /api/plan/v1/messages) - apiFormat → anthropic - rename to 火山方舟 Agent Plan Probe still targets /api/plan/v1/models (the path exists); if the Anthropic gateway doesn't return an OpenAI-shaped list, users fall back to typing a model id like ark-code-latest. * fix(token-plan): Volcengine Agent Plan = OpenAI /api/plan/v3 + ark-code-latest Settled after probing the real key and reading cc-switch's approach: - The ark- plan key works on the OpenAI-compatible /api/plan/v3 endpoint (chat/completions returns 200); switch apiFormat back to openai. - The plan exposes NO /models list (every /api/plan/*/models is 404), which is why probing kept returning 0. cc-switch handles this by hardcoding a single ark-code-latest (an auto-routing alias valid on any tier) and does NOT use AK/SK for model listing — so we do the same. - Seed defaultModels: ['ark-code-latest'] only; users add specific ids by hand. Supporting machinery (kept, general-purpose): - applyTokenPlan seeds models from defaultModels instead of wiping to [] - handleApply uses defaultModels and skips the doomed probe when present - drop stray .playwright-mcp/ debug artifacts and gitignore them * style: fix prettier formatting in usage files CI runs prettier on the whole repo (prettier . --check); these four files predate this branch's formatting pass and tripped the check. * feat(token-plan): verify Volcengine Agent Plan's published model set The Agent Plan publishes a fixed model set but exposes no /models endpoint, so carry the documented models as CANDIDATES and verify each on apply: - add verifyModels flag to TokenPlanModalityTarget - new /api/provider/probe-chat-models route: sends a minimal chat request per candidate (OpenAI /chat/completions or Anthropic /messages) in parallel, returns the subset that succeeds; SSRF-guarded, auth-failure short-circuits - handleApply gains a verify branch (before the fixed-defaultModels fast path), falling back to the seeded list if verification fails - Volcengine preset now carries the 12 published Agent Plan text models (doubao-seed-2.0-*/deepseek-v4-*/minimax-m*/glm-5.2/kimi-k2.*) as candidates This auto-prunes retired (docs flag deepseek-v3.2/glm-5.1 as 即将下线) and tier-gated models without code changes. Verified all 12 resolve against a real plan key. * feat(token-plan): wire Volcengine Agent Plan image + video modalities Make the Ark seedream/seedance adapters path-configurable and light up the image/video modalities on the Volcengine plan: - seedream/seedance adapters: resolveArkRoot() uses baseUrl verbatim when it already carries an /api/... path (token plan's /api/plan/v3), else appends the standard /api/v3 — no regression for the pay-as-you-go default host. - applyTokenPlan: image/video branches inject a modality's defaultModels as customModels and set them as the active provider+model, so generation works out of the box. New optional setImageProvider/ModelId + setVideoProvider/ ModelId actions (UI passes the store setters; tests omit them). - Volcengine preset declares image (doubao-seedream-5.0-lite, verified 200 on /api/plan/v3/images/generations) and video (doubao-seedance-2.0/1.5-pro — Medium+ tiers only; lower tiers reject at call time, no code change needed to upgrade). Applying the plan overwrites the shared seedream/seedance slot with the plan config (same overwrite model as LLM); switching back to pay-as-you-go is a manual edit or plan removal. Verified image end-to-end with a real plan key. * feat(token-plan): verify image/video models on apply, disable unsupported tiers The Volcengine plan lit up video optimistically, but lower tiers (Small) don't include video — so using it 404'd with UnsupportedModel. Probe media models on apply and only keep what the tier actually supports: - generalize /api/provider/probe-chat-models with a `kind` (chat|image|video): image hits /images/generations, video hits /contents/generations/tasks with empty content. The model-support check (404 UnsupportedModel) runs before any billable work, so probing never starts a real image/video job; for media, "supported" = any non-404 response. - handleApply: after lighting up image/video, probe each verifyModels modality; prune to the verified model set + re-select a working model, or disable the modality entirely if none pass (no false "available"). - Volcengine preset: image/video targets gain verifyModels: true. - add settings.tokenPlan.tierUnsupported across 8 locales. Verified with a real Small-tier key: image (seedream-5.0-lite) passes and is kept; video (seedance-2.0/1.5-pro) 404s and is disabled. * feat(web-search): add Doubao (豆包搜索) provider Doubao Search (Custom 版) over its REST endpoint POST open.feedcoopapi.com/search_api/web_search with Bearer auth — the same endpoint the askecho-search-infinity MCP server wraps, so the Volcengine Agent Plan key authenticates directly. Mirrors the MiniMax adapter: maps Result.WebResults to WebSearchSource (prefers Summary, the query-relevant excerpt, over Snippet for LLM use) and surfaces errors from ResponseMetadata.Error. - register 'doubao' in WebSearchProviderId + WEB_SEARCH_PROVIDERS - searchWithDoubao adapter, searchWeb dispatch, store default config - SSRF allowlist entry for the search host * feat(audio): support Agent Plan single-key auth for Doubao TTS generateDoubaoTTS now picks auth + endpoint from the key shape, since Volcengine exposes Seed-TTS as two products with separate credentials (verified: a plan key 401s on the normal endpoint, and the plan endpoint rejects appId-style auth): - single key (no colon) -> X-Api-Key, for the Agent Plan /plan endpoint - appId:accessKey -> X-Api-App-Id + X-Api-Access-Key (unchanged) A malformed pair (empty half) fails clearly instead of sending an empty header. Reuses the existing NDJSON/base64-mp3 parsing and voice list. * fix(media): map MiniMax video 720p to its real 768P tier normalizeVideoOptions defaults minimax-video to '720p' (the first supported resolution), but Hailuo 2.3 only accepts 768P/1080P and rejects 720P with '2013 ... does not support resolution 720P'. MiniMax's mid tier is 768P, not 720P (the adapter already falls back to 768P, as does the connectivity test), so map the shared enum's '720p' to 768P. Regression tests lock the mapping. * feat(token-plan): add web search + TTS to Volcengine Agent Plan, widen image tiers Extend the volcengine-ark preset now that the adapters exist: - webSearch -> doubao (own host open.feedcoopapi.com, not the ark endpoint) - tts -> doubao-tts on the /api/plan/tts endpoint (single-key auth) - image defaultModels widened to a best-first Seedream 5.0/4.5/4.0 list so a higher tier keeps the strongest model while verifyModels prunes the rest; video keeps the 2.0 + 1.5-pro candidates Comments record the verified host/auth quirks of each modality. * feat(token-plan): show result panel only after probing, with two clear states Addresses review feedback that a green check implied generation works when it only meant 'configured'. The panel now renders after probing finishes (gated on results && !applying) so it reflects the final set, and uses two states: green when the modality is configured/usable, muted when a live probe proved it unavailable (e.g. video on a tier without it). * feat(token-plan): scope presets to true multi-modal token plans Drop the single-modality LLM presets (OpenRouter, SiliconFlow, DeepSeek, GLM, Qwen) from Token Plan. A token plan's defining trait is one key spanning many modalities; those entries are ordinary LLM API providers already covered by the add-provider flow, and listing them here muddied the 'one key, every modality' promise. Only MiniMax and the Volcengine Ark Agent Plan remain. The UI already hides categories with no entries. apply-token-plan's LLM-only test now uses a local fixture instead of the removed deepseek preset. * feat(token-plan): progressive reveal of probe results on apply The result panel previously rendered all at once after probing finished, reading as dead air during the model probe. Now rows appear immediately on Apply: modalities with a live probe in flight show a spinner ('pending') and resolve to lit/failed independently as each probe returns, while non-probe modalities show lit right away. Probes run in parallel (Promise.all) instead of sequentially. A row only turns green once its own probe confirms, so this reveals structure + live progress without a premature green — complementing the earlier 'render only after probing' intent rather than reverting it. Per review feedback from @wyuc on #784. * fix(token-plan): enrich seeded models with built-in thinking capability Token Plan built ModelInfo objects from probed ids via modelInfoFromId(), filling only streaming/tools/vision — so a model that supports configurable thinking lost capabilities.thinking and InlineThinkingControl was hidden. modelInfoFromId now takes an optional providerId and overlays the catalog thinking capability for that (provider, model) pair; applyTokenPlan does the same for its synchronously-seeded list. Added the Ark Agent Plan's dotted aliases to the metadata table: - native Doubao Seed 2.0 family (doubao-seed-2.0-pro/code/lite/mini) - cross-vendor models the plan serves through its OpenAI-compatible endpoint (deepseek-v4-pro/flash, glm-5.2, kimi-k2.7-code/k2.6, minimax-m3/m2.7, ark-code-latest) All verified against a live plan key: each accepts the gateway's unified reasoning_effort field (low/medium/high) and actually reasons. They share the doubao effort adapter, which disables via 'minimal' (not 'none') — matching what the plan endpoint accepts (it rejects reasoning_effort:'none'). Addresses review point #1 from @wyuc on #784. * Improve token plan capability setup UI * fix token plan setup flow * chore: prettier format tts-providers.ts | 3 个月前 | |
[codex] add VoxCPM2 TTS provider (#496) * add voxcpm2 tts provider * address voxcpm review feedback * translate voxcpm locale strings * narrow toolbar model pill * refine toolbar media controls * address voxcpm review followups | 5 个月前 | |
support recent model providers (#481) | 5 个月前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 3 个月前 | ||
| 7 个月前 | ||
| 5 个月前 | ||
| 7 个月前 | ||
| 2 个月前 | ||
| 5 个月前 | ||
| 5 个月前 | ||
| 7 个月前 | ||
| 7 个月前 | ||
| 3 个月前 | ||
| 15 天前 | ||
| 7 个月前 | ||
| 6 个月前 | ||
| 2 个月前 | ||
| 7 个月前 | ||
| 7 个月前 | ||
| 6 个月前 | ||
| 5 个月前 | ||
| 7 个月前 | ||
| 7 个月前 | ||
| 5 个月前 | ||
| 7 个月前 | ||
| 7 个月前 | ||
| 6 个月前 | ||
| 6 个月前 | ||
| 5 个月前 | ||
| 7 个月前 | ||
| 7 个月前 | ||
| 5 个月前 | ||
| 1 个月前 | ||
| 7 个月前 | ||
| 3 个月前 | ||
| 5 个月前 | ||
| 5 个月前 |