From b975a5c5c399e7fb888154552f7e8b5b184d104e Mon Sep 17 00:00:00 2001 From: aiapienthusiast <327629069+aiapienthusiast@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:11:45 +0400 Subject: [PATCH 1/2] feat: add Cheaper Inference memory provider preset --- README.ar.md | 8 +- README.de.md | 8 +- README.md | 8 +- README.nl.md | 8 +- README.tr.md | 8 +- README.zh.md | 8 +- opencode-mem.example.jsonc | 13 +- src/config.ts | 49 ++++- src/services/ai/ai-provider-factory.ts | 5 + src/services/ai/provider-config.ts | 4 +- .../ai/providers/cheaper-inference.ts | 68 ++++++ src/services/ai/session/session-types.ts | 1 + src/types/index.ts | 1 + tests/ai-provider-config.test.ts | 24 ++ tests/cheaper-inference-provider.test.ts | 206 ++++++++++++++++++ tests/config-resolution.test.ts | 54 +++++ 16 files changed, 457 insertions(+), 16 deletions(-) create mode 100644 src/services/ai/providers/cheaper-inference.ts create mode 100644 tests/cheaper-inference-provider.test.ts diff --git a/README.ar.md b/README.ar.md index 0c346e6d..3b8c8da0 100644 --- a/README.ar.md +++ b/README.ar.md @@ -482,6 +482,12 @@ memory({ mode: "import", inputPath: "./memories.json" }); "memoryApiKey": "env://ATLASCLOUD_API_KEY", ``` عند اختيار هذا المزود، تُرسل مطالبات الالتقاط التلقائي / الملف، واستجابات النموذج، وسياق المحادثة ذي الصلة إلى `https://api.atlascloud.ai`. +- `cheaper-inference`: إعداد مسبق لـ OpenAI-compatible Chat Completions لـ [Cheaper Inference](https://cheaperinference.com). `memoryApiUrl` و`memoryModel` اختياريان — الافتراضي `https://api.cheaperinference.com/v1` و`gpt-5.4-mini`. إذا حُذف `memoryApiKey`، يُستخدم `CHEAPER_INFERENCE_API_KEY` من البيئة. مثال: + ```jsonc + "memoryProvider": "cheaper-inference", + "memoryApiKey": "env://CHEAPER_INFERENCE_API_KEY", + ``` + عند اختيار هذا المزود، تُرسل مطالبات الالتقاط التلقائي / الملف، واستجابات النموذج، وسياق المحادثة ذي الصلة إلى `https://api.cheaperinference.com`. استكشاف الأخطاء: @@ -489,7 +495,7 @@ memory({ mode: "import", inputPath: "./memories.json" }); - إذا أبلغ الالتقاط التلقائي أن مزودًا غير متصل، أكّد اسم المزود بـ `opencode providers list` وأعد ذلك المزود في opencode أولًا. - إذا أعاد وكيل أو مزود مخصص نصًا عاديًا بدل مخرجات منظمة/أدوات، اختر نموذجًا/مزودًا آخر أو استخدم أحد أوضاع المزود اليدوية أعلاه. - للنماذج التي ترفض `temperature`، أضف `"memoryTemperature": false` عند استخدام إعداد API اليدوي. -- للنماذج التي ترفض استدعاءات الأدوات المفروضة (`tool_choice: "required"`، مثل بعض أوضاع التفكير)، أضف `"forceToolChoice": false` عند استخدام `openai-chat` / `orcarouter` / `atlas-cloud`. +- للنماذج التي ترفض استدعاءات الأدوات المفروضة (`tool_choice: "required"`، مثل بعض أوضاع التفكير)، أضف `"forceToolChoice": false` عند استخدام `openai-chat` / `orcarouter` / `atlas-cloud` / `cheaper-inference`. - لـ `opencodeProvider` / `opencodeModel` (مثل DeepSeek V4 thinking)، ما زال OpenCode يرسل `tool_choice` مفروضًا للمخرجات المنظمة. يعطّل opencode-mem التفكير على الوكيل الداخلي `opencode-mem-structured` (ويعيده في `chat.params` بعد دمج المتغير) حتى يكتمل الالتقاط التلقائي وتعلّم الملف. وكيل الدردشة التفاعلي لديك دون تغيير. إذا فشل الالتقاط ما زال بخطأ تفكير/`tool_choice`، اختر نموذجًا غير تفكير لـ `opencodeModel` أو أعد رجوعًا يدويًا كاملًا (`memoryModel` + `memoryApiUrl`). - **`opencode-claude-auth` / Claude Code:** يستخدم الالتقاط التلقائي OpenCode مع مزودك `anthropic` المصادق. غالبًا ما يتكرر `format: json_schema` المفروض مع Claude-auth، لذا يستخدم opencode-mem مسار **نص-JSON** يحافظ على المصادقة لـ `opencodeProvider: "anthropic"` (بدون أدوات `StructuredOutput` مفروضة؛ تُحلَّل الرد بـ Zod). ما زال مراقب الخطوات يجهض الجلسات الداخلية الجامحة بعد خطوتين. إذا فشل الالتقاط ما زال، أعد رجوع Anthropic بمفتاح API يدوي كامل (`memoryProvider: "anthropic"` + `memoryModel` + `memoryApiUrl` + `memoryApiKey`) — لا يمكن إعادة استخدام Claude Pro/Max OAuth خارج OpenCode. - **المنصات غير المدعومة:** Intel Mac (`darwin/x64`) غير مدعوم — `@tursodatabase/database` وإصدارات `onnxruntime-node` الثابتة (المثبّت `1.30.0`) لا توفّر ربطًا أصليًا لـ x64. استخدم Apple Silicon أو Linux أو Windows، أو نقطة نهاية تضمين بعيدة عبر `embeddingApiUrl` + `embeddingApiKey`. MLX غير مدعوم. diff --git a/README.de.md b/README.de.md index 16c48a43..b1486d68 100644 --- a/README.de.md +++ b/README.de.md @@ -482,6 +482,12 @@ Manuelle `memoryProvider`-Modi: "memoryApiKey": "env://ATLASCLOUD_API_KEY", ``` Wenn dieser Provider gewählt ist, werden Auto-Capture- / Profil-Prompts, Modellantworten und relevanter Gesprächskontext an `https://api.atlascloud.ai` übertragen. +- `cheaper-inference`: OpenAI-kompatibles Chat-Completions-Preset für [Cheaper Inference](https://cheaperinference.com). `memoryApiUrl` und `memoryModel` sind optional — sie defaulten auf `https://api.cheaperinference.com/v1` und `gpt-5.4-mini`. Fehlt `memoryApiKey`, wird `CHEAPER_INFERENCE_API_KEY` aus der Umgebung verwendet. Beispiel: + ```jsonc + "memoryProvider": "cheaper-inference", + "memoryApiKey": "env://CHEAPER_INFERENCE_API_KEY", + ``` + Wenn dieser Provider gewählt ist, werden Auto-Capture- / Profil-Prompts, Modellantworten und relevanter Gesprächskontext an `https://api.cheaperinference.com` übertragen. Fehlerbehebung: @@ -489,7 +495,7 @@ Fehlerbehebung: - Meldet Auto-Capture, dass ein Provider nicht verbunden ist, den Provider-Namen mit `opencode providers list` bestätigen und diesen Provider zuerst in opencode konfigurieren. - Gibt ein Proxy oder Custom-Provider Klartext statt strukturierter/Tool-Ausgabe zurück, ein anderes Modell/einen anderen Provider wählen oder einen der manuellen Provider-Modi oben nutzen. - Für Modelle, die `temperature` ablehnen, bei manueller API-Konfiguration `"memoryTemperature": false` hinzufügen. -- Für Modelle, die erzwungene Tool-Calls ablehnen (`tool_choice: "required"`, z. B. manche Thinking-Modi), bei `openai-chat` / `orcarouter` / `atlas-cloud` `"forceToolChoice": false` hinzufügen. +- Für Modelle, die erzwungene Tool-Calls ablehnen (`tool_choice: "required"`, z. B. manche Thinking-Modi), bei `openai-chat` / `orcarouter` / `atlas-cloud` / `cheaper-inference` `"forceToolChoice": false` hinzufügen. - Bei `opencodeProvider` / `opencodeModel` (z. B. DeepSeek V4 Thinking) sendet OpenCode weiterhin erzwungenes `tool_choice` für strukturierte Ausgabe. opencode-mem deaktiviert Thinking auf dem internen Agenten `opencode-mem-structured` (und wendet das in `chat.params` nach Variant-Merge erneut an), damit Auto-Capture und Profil-Lernen abschließen können. Dein interaktiver Chat-Agent bleibt unverändert. Scheitert Capture weiterhin mit einem Thinking-/`tool_choice`-Fehler, für `opencodeModel` ein Non-Thinking-Modell wählen oder einen vollständigen manuellen Fallback konfigurieren (`memoryModel` + `memoryApiUrl`). - **`opencode-claude-auth` / Claude Code:** Auto-Capture nutzt OpenCode mit deinem authentifizierten `anthropic`-Provider. Erzwungenes `format: json_schema` loopt oft mit Claude-Auth, daher nutzt opencode-mem einen Auth-erhaltenden **Text-JSON**-Pfad für `opencodeProvider: "anthropic"` (keine erzwungenen `StructuredOutput`-Tools; Antwort wird mit Zod geparst). Ein Step-Watchdog bricht runaway interne Sitzungen nach 2 Schritten weiterhin ab. Scheitert Capture weiterhin, einen vollständigen manuellen Anthropic-API-Key-Fallback konfigurieren (`memoryProvider: "anthropic"` + `memoryModel` + `memoryApiUrl` + `memoryApiKey`) — Claude Pro/Max OAuth lässt sich außerhalb von OpenCode nicht wiederverwenden. - **Nicht unterstützte Plattformen:** Intel Mac (`darwin/x64`) wird nicht unterstützt — `@tursodatabase/database` und feste `onnxruntime-node`-Releases (gepinnt `1.30.0`) liefern kein natives x64-Binding. Apple Silicon, Linux oder Windows nutzen, oder einen Remote-Embedding-Endpoint über `embeddingApiUrl` + `embeddingApiKey`. MLX wird nicht unterstützt. diff --git a/README.md b/README.md index 7abce242..7f17d8cf 100644 --- a/README.md +++ b/README.md @@ -491,6 +491,12 @@ Manual `memoryProvider` modes: "memoryApiKey": "env://ATLASCLOUD_API_KEY", ``` When this provider is selected, auto-capture / profile prompts, model responses, and relevant conversation context are transmitted to `https://api.atlascloud.ai`. +- `cheaper-inference`: OpenAI-compatible Chat Completions preset for [Cheaper Inference](https://cheaperinference.com). `memoryApiUrl` and `memoryModel` are optional — they default to `https://api.cheaperinference.com/v1` and `gpt-5.4-mini`. If `memoryApiKey` is omitted, `CHEAPER_INFERENCE_API_KEY` from the environment is used. Example: + ```jsonc + "memoryProvider": "cheaper-inference", + "memoryApiKey": "env://CHEAPER_INFERENCE_API_KEY", + ``` + When this provider is selected, auto-capture / profile prompts, model responses, and relevant conversation context are transmitted to `https://api.cheaperinference.com`. Troubleshooting: @@ -498,7 +504,7 @@ Troubleshooting: - If auto-capture reports that a provider is not connected, confirm the provider name with `opencode providers list` and configure that provider in opencode first. - If a proxy or custom provider returns plain text instead of structured/tool output, choose another model/provider or use one of the manual provider modes above. - For models that reject `temperature`, add `"memoryTemperature": false` when using manual API configuration. -- For models that reject forced tool calls (`tool_choice: "required"`, e.g. some thinking modes), add `"forceToolChoice": false` when using `openai-chat` / `orcarouter` / `atlas-cloud`. +- For models that reject forced tool calls (`tool_choice: "required"`, e.g. some thinking modes), add `"forceToolChoice": false` when using `openai-chat` / `orcarouter` / `atlas-cloud` / `cheaper-inference`. - For `opencodeProvider` / `opencodeModel` (e.g. DeepSeek V4 thinking), OpenCode still sends forced `tool_choice` for structured output. opencode-mem disables thinking on the internal `opencode-mem-structured` agent (and re-applies that in `chat.params` after variant merge) so auto-capture and profile learning can complete. Your interactive chat agent is unchanged. If capture still fails with a thinking/`tool_choice` error, pick a non-thinking model for `opencodeModel` or configure a complete manual fallback (`memoryModel` + `memoryApiUrl`). - **`opencode-claude-auth` / Claude Code:** auto-capture uses OpenCode with your authenticated `anthropic` provider. Forced `format: json_schema` often loops with Claude-auth, so opencode-mem uses an auth-preserving **text-JSON** path for `opencodeProvider: "anthropic"` (no forced `StructuredOutput` tools; reply is parsed with Zod). A step watchdog still aborts runaway internal sessions after 2 steps. If capture still fails, configure a complete manual Anthropic API-key fallback (`memoryProvider: "anthropic"` + `memoryModel` + `memoryApiUrl` + `memoryApiKey`) — Claude Pro/Max OAuth cannot be reused outside OpenCode. - **Unsupported platforms:** Intel Mac (`darwin/x64`) is not supported — `@tursodatabase/database` and fixed `onnxruntime-node` releases (pinned `1.30.0`) ship no x64 native binding. Use Apple Silicon, Linux, or Windows, or a remote embedding endpoint via `embeddingApiUrl` + `embeddingApiKey`. MLX is not supported. diff --git a/README.nl.md b/README.nl.md index f5658da1..f8f9ab39 100644 --- a/README.nl.md +++ b/README.nl.md @@ -483,6 +483,12 @@ Handmatige `memoryProvider`-modi: "memoryApiKey": "env://ATLASCLOUD_API_KEY", ``` Wanneer deze provider is geselecteerd, worden auto-capture- / profielprompts, modelantwoorden en relevante conversatiecontext verzonden naar `https://api.atlascloud.ai`. +- `cheaper-inference`: OpenAI-compatibel Chat Completions-preset voor [Cheaper Inference](https://cheaperinference.com). `memoryApiUrl` en `memoryModel` zijn optioneel — ze defaulten naar `https://api.cheaperinference.com/v1` en `gpt-5.4-mini`. Als `memoryApiKey` wordt weggelaten, wordt `CHEAPER_INFERENCE_API_KEY` uit de omgeving gebruikt. Voorbeeld: + ```jsonc + "memoryProvider": "cheaper-inference", + "memoryApiKey": "env://CHEAPER_INFERENCE_API_KEY", + ``` + Wanneer deze provider is geselecteerd, worden auto-capture- / profielprompts, modelantwoorden en relevante conversatiecontext verzonden naar `https://api.cheaperinference.com`. Probleemoplossing: @@ -490,7 +496,7 @@ Probleemoplossing: - Als auto-capture meldt dat een provider niet verbonden is, bevestig de providernaam met `opencode providers list` en configureer die provider eerst in opencode. - Als een proxy of custom provider platte tekst teruggeeft in plaats van structured/tool-output, kies een ander model/provider of gebruik een van de handmatige providermodi hierboven. - Voor modellen die `temperature` weigeren, voeg `"memoryTemperature": false` toe bij handmatige API-configuratie. -- Voor modellen die geforceerde tool calls weigeren (`tool_choice: "required"`, bijv. sommige thinking-modi), voeg `"forceToolChoice": false` toe bij `openai-chat` / `orcarouter` / `atlas-cloud`. +- Voor modellen die geforceerde tool calls weigeren (`tool_choice: "required"`, bijv. sommige thinking-modi), voeg `"forceToolChoice": false` toe bij `openai-chat` / `orcarouter` / `atlas-cloud` / `cheaper-inference`. - Voor `opencodeProvider` / `opencodeModel` (bijv. DeepSeek V4 thinking) stuurt OpenCode nog steeds geforceerde `tool_choice` voor structured output. opencode-mem schakelt thinking uit op de interne `opencode-mem-structured`-agent (en past dat opnieuw toe in `chat.params` na variant-merge) zodat auto-capture en profiel-leren kunnen voltooien. Je interactieve chatagent blijft ongewijzigd. Als capture nog steeds faalt met een thinking/`tool_choice`-fout, kies een non-thinking model voor `opencodeModel` of configureer een complete handmatige fallback (`memoryModel` + `memoryApiUrl`). - **`opencode-claude-auth` / Claude Code:** auto-capture gebruikt OpenCode met je geauthenticeerde `anthropic`-provider. Geforceerde `format: json_schema` loopt vaak vast met Claude-auth, dus opencode-mem gebruikt een auth-behoudend **text-JSON**-pad voor `opencodeProvider: "anthropic"` (geen geforceerde `StructuredOutput`-tools; antwoord wordt met Zod geparsed). Een step-watchdog breekt runaway interne sessies nog steeds af na 2 stappen. Als capture nog steeds faalt, configureer een complete handmatige Anthropic API-key-fallback (`memoryProvider: "anthropic"` + `memoryModel` + `memoryApiUrl` + `memoryApiKey`) — Claude Pro/Max OAuth kan niet buiten OpenCode worden hergebruikt. - **Niet-ondersteunde platforms:** Intel Mac (`darwin/x64`) wordt niet ondersteund — `@tursodatabase/database` en vaste `onnxruntime-node`-releases (vastgezet `1.30.0`) leveren geen x64 native binding. Gebruik Apple Silicon, Linux of Windows, of een remote embedding-endpoint via `embeddingApiUrl` + `embeddingApiKey`. MLX wordt niet ondersteund. diff --git a/README.tr.md b/README.tr.md index 445a5b3f..e9947901 100644 --- a/README.tr.md +++ b/README.tr.md @@ -484,6 +484,12 @@ Manuel `memoryProvider` modları: "memoryApiKey": "env://ATLASCLOUD_API_KEY", ``` Bu sağlayıcı seçildiğinde otomatik yakalama / profil prompt’ları, model yanıtları ve ilgili konuşma bağlamı `https://api.atlascloud.ai` adresine iletilir. +- `cheaper-inference`: [Cheaper Inference](https://cheaperinference.com) için OpenAI uyumlu Chat Completions ön ayarı. `memoryApiUrl` ve `memoryModel` isteğe bağlıdır — varsayılanları `https://api.cheaperinference.com/v1` ve `gpt-5.4-mini`’dir. `memoryApiKey` atlanırsa ortamdan `CHEAPER_INFERENCE_API_KEY` kullanılır. Örnek: + ```jsonc + "memoryProvider": "cheaper-inference", + "memoryApiKey": "env://CHEAPER_INFERENCE_API_KEY", + ``` + Bu sağlayıcı seçildiğinde otomatik yakalama / profil prompt’ları, model yanıtları ve ilgili konuşma bağlamı `https://api.cheaperinference.com` adresine iletilir. Sorun giderme: @@ -491,7 +497,7 @@ Sorun giderme: - Otomatik yakalama bir sağlayıcının bağlı olmadığını bildirirse, sağlayıcı adını `opencode providers list` ile doğrulayın ve önce o sağlayıcıyı opencode’da yapılandırın. - Bir proxy veya özel sağlayıcı yapılandırılmış/araç çıktısı yerine düz metin döndürürse, başka bir model/sağlayıcı seçin veya yukarıdaki manuel sağlayıcı modlarından birini kullanın. - `temperature` reddeden modeller için manuel API yapılandırması kullanırken `"memoryTemperature": false` ekleyin. -- Zorlanmış araç çağrılarını (`tool_choice: "required"`, örn. bazı düşünme modları) reddeden modeller için `openai-chat` / `orcarouter` / `atlas-cloud` kullanırken `"forceToolChoice": false` ekleyin. +- Zorlanmış araç çağrılarını (`tool_choice: "required"`, örn. bazı düşünme modları) reddeden modeller için `openai-chat` / `orcarouter` / `atlas-cloud` / `cheaper-inference` kullanırken `"forceToolChoice": false` ekleyin. - `opencodeProvider` / `opencodeModel` için (örn. DeepSeek V4 thinking), OpenCode yapılandırılmış çıktı için hâlâ zorlanmış `tool_choice` gönderir. opencode-mem dahili `opencode-mem-structured` ajanında düşünmeyi devre dışı bırakır (ve varyant birleştirmesinden sonra bunu `chat.params` içinde yeniden uygular); böylece otomatik yakalama ve profil öğrenme tamamlanabilir. Etkileşimli sohbet ajanınız değişmez. Yakalama hâlâ bir thinking/`tool_choice` hatasıyla başarısız olursa, `opencodeModel` için düşünmeyen bir model seçin veya eksiksiz bir manuel yedek yapılandırın (`memoryModel` + `memoryApiUrl`). - **`opencode-claude-auth` / Claude Code:** otomatik yakalama, kimlik doğrulanmış `anthropic` sağlayıcınızla OpenCode kullanır. Zorlanmış `format: json_schema` Claude-auth ile sık sık döngüye girer; bu nedenle opencode-mem `opencodeProvider: "anthropic"` için kimlik doğrulamayı koruyan bir **metin-JSON** yolu kullanır (zorlanmış `StructuredOutput` araçları yok; yanıt Zod ile ayrıştırılır). Bir adım watchdog’u hâlâ 2 adımdan sonra kaçak dahili oturumları iptal eder. Yakalama hâlâ başarısız olursa, eksiksiz bir manuel Anthropic API anahtarı yedeği yapılandırın (`memoryProvider: "anthropic"` + `memoryModel` + `memoryApiUrl` + `memoryApiKey`) — Claude Pro/Max OAuth OpenCode dışında yeniden kullanılamaz. - **Desteklenmeyen platformlar:** Intel Mac (`darwin/x64`) desteklenmez — `@tursodatabase/database` ve sabit `onnxruntime-node` sürümleri (sabitlenmiş `1.30.0`) x64 yerel bağlayıcı yayınlamaz. Apple Silicon, Linux veya Windows kullanın; ya da `embeddingApiUrl` + `embeddingApiKey` ile uzak bir embedding uç noktası. MLX desteklenmez. diff --git a/README.zh.md b/README.zh.md index 61fa4e7f..4c9e6d42 100644 --- a/README.zh.md +++ b/README.zh.md @@ -479,6 +479,12 @@ memory({ mode: "import", inputPath: "./memories.json" }); "memoryApiKey": "env://ATLASCLOUD_API_KEY", ``` 选择此提供商时,自动捕获 / 画像提示、模型响应与相关对话上下文会传输到 `https://api.atlascloud.ai`。 +- `cheaper-inference`:面向 [Cheaper Inference](https://cheaperinference.com) 的 OpenAI 兼容 Chat Completions 预设。`memoryApiUrl` 与 `memoryModel` 可选 — 它们默认为 `https://api.cheaperinference.com/v1` 与 `gpt-5.4-mini`。若省略 `memoryApiKey`,则使用环境中的 `CHEAPER_INFERENCE_API_KEY`。示例: + ```jsonc + "memoryProvider": "cheaper-inference", + "memoryApiKey": "env://CHEAPER_INFERENCE_API_KEY", + ``` + 选择此提供商时,自动捕获 / 画像提示、模型响应与相关对话上下文会传输到 `https://api.cheaperinference.com`。 故障排除: @@ -486,7 +492,7 @@ memory({ mode: "import", inputPath: "./memories.json" }); - 若自动捕获报告提供商未连接,请用 `opencode providers list` 确认提供商名称,并先在 opencode 中配置该提供商。 - 若代理或自定义提供商返回纯文本而非结构化/工具输出,请选择另一模型/提供商,或使用上方手动提供商模式之一。 - 对于拒绝 `temperature` 的模型,在使用手动 API 配置时添加 `"memoryTemperature": false`。 -- 对于拒绝强制工具调用(`tool_choice: "required"`,例如某些思考模式)的模型,在使用 `openai-chat` / `orcarouter` / `atlas-cloud` 时添加 `"forceToolChoice": false`。 +- 对于拒绝强制工具调用(`tool_choice: "required"`,例如某些思考模式)的模型,在使用 `openai-chat` / `orcarouter` / `atlas-cloud` / `cheaper-inference` 时添加 `"forceToolChoice": false`。 - 对于 `opencodeProvider` / `opencodeModel`(例如 DeepSeek V4 thinking),OpenCode 仍会为结构化输出发送强制的 `tool_choice`。opencode-mem 在内部 `opencode-mem-structured` 代理上禁用思考(并在变体合并后在 `chat.params` 中重新应用),以便自动捕获与画像学习可以完成。你的交互式聊天代理不变。若捕获仍因 thinking/`tool_choice` 错误失败,请为 `opencodeModel` 选择非思考模型,或配置完整的手动回退(`memoryModel` + `memoryApiUrl`)。 - **`opencode-claude-auth` / Claude Code:** 自动捕获使用带有你已认证 `anthropic` 提供商的 OpenCode。强制的 `format: json_schema` 常与 Claude-auth 循环,因此 opencode-mem 对 `opencodeProvider: "anthropic"` 使用保留认证的 **text-JSON** 路径(无强制 `StructuredOutput` 工具;用 Zod 解析回复)。步骤看门狗仍会在 2 步后中止失控的内部会话。若捕获仍失败,请配置完整的手动 Anthropic API 密钥回退(`memoryProvider: "anthropic"` + `memoryModel` + `memoryApiUrl` + `memoryApiKey`)— Claude Pro/Max OAuth 无法在 OpenCode 外重用。 - **不支持的平台:** 不支持 Intel Mac(`darwin/x64`) — `@tursodatabase/database` 与固定版本的 `onnxruntime-node` 发行版(固定 `1.30.0`)不提供 x64 原生绑定。请使用 Apple Silicon、Linux 或 Windows,或通过 `embeddingApiUrl` + `embeddingApiKey` 使用远程嵌入端点。不支持 MLX。 diff --git a/opencode-mem.example.jsonc b/opencode-mem.example.jsonc index 50f6fb9c..c4bbd8a3 100644 --- a/opencode-mem.example.jsonc +++ b/opencode-mem.example.jsonc @@ -182,7 +182,7 @@ "autoCaptureEnabled": true, - // Provider type: "atlas-cloud" | "openai-chat" | "openai-responses" | "anthropic" | "minimax" | "orcarouter" + // Provider type: "atlas-cloud" | "cheaper-inference" | "openai-chat" | "openai-responses" | "anthropic" | "minimax" | "orcarouter" // Note: "openai-chat" is a generic OpenAI API-compatible mode. // Any service that follows the OpenAI Chat Completions API can use it via custom "memoryApiUrl". "memoryProvider": "openai-chat", @@ -255,6 +255,15 @@ // // conversation context are sent to api.atlascloud.ai. // // "memoryModel": "deepseek-ai/deepseek-v4-pro" + // Cheaper Inference (OpenAI-compatible Chat Completions preset, with session support): + // "memoryProvider": "cheaper-inference" + // "memoryApiKey": "env://CHEAPER_INFERENCE_API_KEY" + // // memoryApiUrl and memoryModel are optional — they default to + // // https://api.cheaperinference.com/v1 and "gpt-5.4-mini". + // // When selected, auto-capture/profile prompts, responses, and relevant + // // conversation context are sent to api.cheaperinference.com. + // // "memoryModel": "gpt-5.4-mini" + // Groq (OpenAI-compatible, use openai-chat provider): // "memoryProvider": "openai-chat" // "memoryModel": "llama-3.3-70b-versatile" @@ -283,7 +292,7 @@ // Set to false and add "memoryTemperature": false in config when using such models "memoryTemperature": 0.3, - // Force tool calls on openai-chat / orcarouter / atlas-cloud (tool_choice: "required"). Default true. + // Force tool calls on openai-chat / orcarouter / atlas-cloud / cheaper-inference (tool_choice: "required"). Default true. // Some thinking/reasoning models reject forced tool choice — set false to fall back to "auto": // "forceToolChoice": false, diff --git a/src/config.ts b/src/config.ts index ac9ac210..3be957cc 100644 --- a/src/config.ts +++ b/src/config.ts @@ -66,7 +66,13 @@ interface OpenCodeMemConfig { autoCaptureMaxContextBytes?: number; autoCaptureLanguage?: string; memoryProvider?: - "atlas-cloud" | "openai-chat" | "openai-responses" | "anthropic" | "minimax" | "orcarouter"; + | "atlas-cloud" + | "cheaper-inference" + | "openai-chat" + | "openai-responses" + | "anthropic" + | "minimax" + | "orcarouter"; memoryModel?: string; memoryApiUrl?: string; memoryApiKey?: string; @@ -210,7 +216,13 @@ const DEFAULTS: Required< memoryApiUrl?: string; memoryApiKey?: string; memoryProvider?: - "atlas-cloud" | "openai-chat" | "openai-responses" | "anthropic" | "minimax" | "orcarouter"; + | "atlas-cloud" + | "cheaper-inference" + | "openai-chat" + | "openai-responses" + | "anthropic" + | "minimax" + | "orcarouter"; memoryTemperature?: number | false; forceToolChoice?: boolean; memoryExtraParams?: Record; @@ -487,7 +499,7 @@ const CONFIG_TEMPLATE = `{ "autoCaptureEnabled": true, - // Provider type: "atlas-cloud" | "openai-chat" | "openai-responses" | "anthropic" | "minimax" | "orcarouter" + // Provider type: "atlas-cloud" | "cheaper-inference" | "openai-chat" | "openai-responses" | "anthropic" | "minimax" | "orcarouter" // Note: "openai-chat" is a generic OpenAI API-compatible mode. // Any service that follows the OpenAI Chat Completions API can use it via custom "memoryApiUrl". "memoryProvider": "openai-chat", @@ -560,6 +572,15 @@ const CONFIG_TEMPLATE = `{ // // conversation context are sent to api.atlascloud.ai. // // "memoryModel": "deepseek-ai/deepseek-v4-pro" + // Cheaper Inference (OpenAI-compatible Chat Completions preset, with session support): + // "memoryProvider": "cheaper-inference" + // "memoryApiKey": "env://CHEAPER_INFERENCE_API_KEY" + // // memoryApiUrl and memoryModel are optional — they default to + // // https://api.cheaperinference.com/v1 and "gpt-5.4-mini". + // // When selected, auto-capture/profile prompts, responses, and relevant + // // conversation context are sent to api.cheaperinference.com. + // // "memoryModel": "gpt-5.4-mini" + // Groq (OpenAI-compatible, use openai-chat provider): // "memoryProvider": "openai-chat" // "memoryModel": "llama-3.3-70b-versatile" @@ -588,7 +609,7 @@ const CONFIG_TEMPLATE = `{ // Set to false and add "memoryTemperature": false in config when using such models "memoryTemperature": 0.3, - // Force tool calls on openai-chat / orcarouter / atlas-cloud (tool_choice: "required"). Default true. + // Force tool calls on openai-chat / orcarouter / atlas-cloud / cheaper-inference (tool_choice: "required"). Default true. // Some thinking/reasoning models reject forced tool choice — set false to fall back to "auto": // "forceToolChoice": false, @@ -960,10 +981,20 @@ export function normalizeInjectionMarkers(value: string[] | undefined): string[] function buildConfig(fileConfig: OpenCodeMemConfig) { const memoryProvider = (fileConfig.memoryProvider ?? "openai-chat") as - "atlas-cloud" | "openai-chat" | "openai-responses" | "anthropic" | "minimax" | "orcarouter"; + | "atlas-cloud" + | "cheaper-inference" + | "openai-chat" + | "openai-responses" + | "anthropic" + | "minimax" + | "orcarouter"; const memoryApiKey = resolveSecretValue(fileConfig.memoryApiKey) ?? - (memoryProvider === "atlas-cloud" ? process.env.ATLASCLOUD_API_KEY : undefined); + (memoryProvider === "atlas-cloud" + ? process.env.ATLASCLOUD_API_KEY + : memoryProvider === "cheaper-inference" + ? process.env.CHEAPER_INFERENCE_API_KEY + : undefined); const embeddingModel = fileConfig.embeddingModel ?? DEFAULTS.embeddingModel; const embeddingPreset = getEmbeddingModelPreset(embeddingModel); const embeddingDimensions = @@ -1186,7 +1217,11 @@ export function getAutoCaptureProviderStatus( // Preset providers fill endpoint/model themselves, so only an API key is // required for the manual fallback path. - if (config.memoryProvider === "orcarouter" || config.memoryProvider === "atlas-cloud") { + if ( + config.memoryProvider === "orcarouter" || + config.memoryProvider === "atlas-cloud" || + config.memoryProvider === "cheaper-inference" + ) { if (!hasMemoryApiKey) issues.push("memoryApiKey is not configured"); if (hasPlaceholderMemoryApiKey) issues.push("memoryApiKey contains a placeholder value"); if (hasMemoryApiKey && !hasPlaceholderMemoryApiKey) { diff --git a/src/services/ai/ai-provider-factory.ts b/src/services/ai/ai-provider-factory.ts index bd81e400..94e0b207 100644 --- a/src/services/ai/ai-provider-factory.ts +++ b/src/services/ai/ai-provider-factory.ts @@ -5,6 +5,7 @@ import { AnthropicMessagesProvider } from "./providers/anthropic-messages.js"; import { MiniMaxProvider } from "./providers/minimax.js"; import { GoogleGeminiProvider } from "./providers/google-gemini.js"; import { AtlasCloudProvider } from "./providers/atlas-cloud.js"; +import { CheaperInferenceProvider } from "./providers/cheaper-inference.js"; import { OrcaRouterProvider } from "./providers/orcarouter.js"; import { aiSessionManager } from "./session/ai-session-manager.js"; import type { AIProviderType } from "./session/session-types.js"; @@ -15,6 +16,9 @@ export class AIProviderFactory { case "atlas-cloud": return new AtlasCloudProvider(config, aiSessionManager); + case "cheaper-inference": + return new CheaperInferenceProvider(config, aiSessionManager); + case "openai-chat": return new OpenAIChatCompletionProvider(config, aiSessionManager); @@ -41,6 +45,7 @@ export class AIProviderFactory { static getSupportedProviders(): AIProviderType[] { return [ "atlas-cloud", + "cheaper-inference", "openai-chat", "openai-responses", "anthropic", diff --git a/src/services/ai/provider-config.ts b/src/services/ai/provider-config.ts index b3afa848..0420136a 100644 --- a/src/services/ai/provider-config.ts +++ b/src/services/ai/provider-config.ts @@ -30,7 +30,9 @@ export function buildMemoryProviderConfig( // Preset providers fill endpoint/model themselves, so memoryModel / // memoryApiUrl are optional there. An API key is always required. const isPresetProvider = - config.memoryProvider === "orcarouter" || config.memoryProvider === "atlas-cloud"; + config.memoryProvider === "orcarouter" || + config.memoryProvider === "atlas-cloud" || + config.memoryProvider === "cheaper-inference"; if (!memoryModel && !isPresetProvider) issues.push("missing memoryModel"); if (!memoryApiUrl && !isPresetProvider) issues.push("missing memoryApiUrl"); diff --git a/src/services/ai/providers/cheaper-inference.ts b/src/services/ai/providers/cheaper-inference.ts new file mode 100644 index 00000000..21ea11bf --- /dev/null +++ b/src/services/ai/providers/cheaper-inference.ts @@ -0,0 +1,68 @@ +import { OpenAIChatCompletionProvider } from "./openai-chat-completion.js"; +import type { ProviderConfig } from "./base-provider.js"; +import type { AISessionManager } from "../session/ai-session-manager.js"; +import type { AIProviderType } from "../session/session-types.js"; + +/** Cheaper Inference OpenAI-compatible endpoint used when `memoryApiUrl` is omitted. */ +export const CHEAPER_INFERENCE_API_URL = "https://api.cheaperinference.com/v1"; + +/** + * Default model when `memoryModel` is omitted. Cheaper Inference model IDs are + * bare (e.g. `gpt-5.4-mini`, `claude-sonnet-5`). + */ +export const CHEAPER_INFERENCE_DEFAULT_MODEL = "gpt-5.4-mini"; + +/** + * Cheaper Inference provider. + * + * [Cheaper Inference](https://cheaperinference.com) exposes an OpenAI-compatible + * Chat Completions API. This provider reuses the OpenAI Chat Completions request + * handling and only overrides the resolved endpoint, default model, and the + * session provider tag, so Cheaper Inference sessions are distinguishable in the + * session store and diagnostics. + * + * Users configure it as: + * "memoryProvider": "cheaper-inference" + * "memoryApiKey": "env://CHEAPER_INFERENCE_API_KEY" + * + * `memoryApiUrl` and `memoryModel` are optional — they default to the Cheaper + * Inference endpoint and `gpt-5.4-mini` respectively. + * + * When this provider is selected, auto-capture / profile prompts, model + * responses, and relevant conversation context are transmitted to + * `https://api.cheaperinference.com`. + */ +export class CheaperInferenceProvider extends OpenAIChatCompletionProvider { + constructor(config: ProviderConfig, aiSessionManager: AISessionManager) { + super(config, aiSessionManager); + } + + override getProviderName(): string { + return "cheaper-inference"; + } + + protected override sessionProviderTag(): AIProviderType { + return "cheaper-inference"; + } + + /** + * Resolve the OpenAI-compatible endpoint. + * + * Defaults to the Cheaper Inference API when `memoryApiUrl` is not configured, + * so a minimal config only needs `memoryProvider` + `memoryApiKey`. + */ + override resolveEndpoint(): string { + const base = (this.config.apiUrl || "").trim().replace(/\/+$/, ""); + return base || CHEAPER_INFERENCE_API_URL; + } + + /** + * Resolve the model ID to send to Cheaper Inference. + * + * Defaults to `gpt-5.4-mini` when `memoryModel` is not configured. + */ + override resolveModel(): string { + const model = (this.config.model || "").trim(); + return model || CHEAPER_INFERENCE_DEFAULT_MODEL; + } +} diff --git a/src/services/ai/session/session-types.ts b/src/services/ai/session/session-types.ts index a2b0ffab..a9bbf21b 100644 --- a/src/services/ai/session/session-types.ts +++ b/src/services/ai/session/session-types.ts @@ -1,5 +1,6 @@ export type AIProviderType = | "atlas-cloud" + | "cheaper-inference" | "openai-chat" | "openai-responses" | "anthropic" diff --git a/src/types/index.ts b/src/types/index.ts index a40a3d19..d7240cf1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -19,6 +19,7 @@ export interface MemoryMetadata { export type AIProviderType = | "atlas-cloud" + | "cheaper-inference" | "openai-chat" | "openai-responses" | "anthropic" diff --git a/tests/ai-provider-config.test.ts b/tests/ai-provider-config.test.ts index 4c1dccea..13e5ead3 100644 --- a/tests/ai-provider-config.test.ts +++ b/tests/ai-provider-config.test.ts @@ -188,6 +188,30 @@ describe("AI provider config", () => { ).toThrow("missing memoryApiKey"); }); + it("builds cheaper-inference config from only an API key, defaulting model and endpoint", () => { + const providerConfig = buildMemoryProviderConfig({ + memoryProvider: "cheaper-inference", + memoryApiKey: "ci-test-key", + }); + + expect(providerConfig).toEqual({ + model: "", + apiUrl: "", + apiKey: "ci-test-key", + forceToolChoice: undefined, + maxIterations: undefined, + iterationTimeout: undefined, + }); + }); + + it("still requires an API key for the cheaper-inference provider", () => { + expect(() => + buildMemoryProviderConfig({ + memoryProvider: "cheaper-inference", + }) + ).toThrow("missing memoryApiKey"); + }); + it("omits temperature for openai-chat when memoryTemperature is false", async () => { let capturedBody: Record | undefined; globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => { diff --git a/tests/cheaper-inference-provider.test.ts b/tests/cheaper-inference-provider.test.ts new file mode 100644 index 00000000..710ed947 --- /dev/null +++ b/tests/cheaper-inference-provider.test.ts @@ -0,0 +1,206 @@ +import { afterEach, describe, expect, it } from "bun:test"; +import { + CheaperInferenceProvider, + CHEAPER_INFERENCE_API_URL, + CHEAPER_INFERENCE_DEFAULT_MODEL, +} from "../src/services/ai/providers/cheaper-inference.js"; +import { AIProviderFactory } from "../src/services/ai/ai-provider-factory.js"; +import type { ChatCompletionTool } from "../src/services/ai/tools/tool-schema.js"; + +const toolSchema: ChatCompletionTool = { + type: "function", + function: { + name: "save_memories", + description: "Save memories", + parameters: { + type: "object", + properties: {}, + required: [], + }, + }, +}; + +class FakeSessionManager { + private readonly session = { id: "session-1" }; + private readonly messages: any[] = []; + lastCreateSessionArgs: any; + + getSession(sessionId?: string, provider?: string): any { + void sessionId; + void provider; + return null; + } + + createSession(args: any): any { + this.lastCreateSessionArgs = args; + return this.session; + } + + getMessages(): any[] { + return this.messages; + } + + getLastSequence(): number { + return this.messages.length - 1; + } + + addMessage(message: any): void { + this.messages.push(message); + } +} + +function makeProvider( + overrides: Record = {}, + sessionManager = new FakeSessionManager() +) { + return { + provider: new CheaperInferenceProvider( + { + model: "", + apiUrl: "", + apiKey: "ci-test-key", + ...overrides, + }, + sessionManager as any + ), + sessionManager, + }; +} + +describe("CheaperInferenceProvider", () => { + const originalFetch = globalThis.fetch; + + afterEach(() => { + globalThis.fetch = originalFetch; + }); + + it("reports the cheaper-inference provider name", () => { + const { provider } = makeProvider(); + expect(provider.getProviderName()).toBe("cheaper-inference"); + expect(provider.supportsSession()).toBe(true); + }); + + it("defaults to the Cheaper Inference endpoint when apiUrl is not configured", () => { + const { provider } = makeProvider(); + expect(provider.resolveEndpoint()).toBe(CHEAPER_INFERENCE_API_URL); + }); + + it("strips a trailing slash from a configured apiUrl", () => { + const { provider } = makeProvider({ apiUrl: "https://proxy.example.com/v1/" }); + expect(provider.resolveEndpoint()).toBe("https://proxy.example.com/v1"); + }); + + it("defaults to gpt-5.4-mini", () => { + const { provider } = makeProvider(); + expect(provider.resolveModel()).toBe(CHEAPER_INFERENCE_DEFAULT_MODEL); + }); + + it("returns a configured model unchanged", () => { + const { provider } = makeProvider({ model: "claude-sonnet-5" }); + expect(provider.resolveModel()).toBe("claude-sonnet-5"); + }); + + it("records the cheaper-inference session provider tag", async () => { + globalThis.fetch = (async () => + ({ + ok: false, + status: 401, + statusText: "Unauthorized", + text: async () => "login fail", + }) as Response) as typeof fetch; + + const { provider, sessionManager } = makeProvider(); + await provider.executeToolCall("system", "user", toolSchema, "session-id"); + + expect(sessionManager.lastCreateSessionArgs?.provider).toBe("cheaper-inference"); + }); + + it("targets /chat/completions on Cheaper Inference and authenticates with Bearer", async () => { + let capturedUrl: string | undefined; + let capturedHeaders: Record | undefined; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + capturedUrl = String(input); + capturedHeaders = init?.headers as Record; + return { + ok: false, + status: 401, + statusText: "Unauthorized", + text: async () => "login fail", + } as Response; + }) as typeof fetch; + + const { provider } = makeProvider(); + await provider.executeToolCall("system", "user", toolSchema, "session-id"); + + expect(capturedUrl).toBe(`${CHEAPER_INFERENCE_API_URL}/chat/completions`); + expect(capturedHeaders?.["Authorization"]).toBe("Bearer ci-test-key"); + }); + + it("sends the resolved default model in the request body", async () => { + let capturedBody: Record | undefined; + globalThis.fetch = (async (_input: RequestInfo | URL, init?: RequestInit) => { + capturedBody = JSON.parse(String(init?.body ?? "{}")); + return { + ok: false, + status: 401, + statusText: "Unauthorized", + text: async () => "login fail", + } as Response; + }) as typeof fetch; + + const { provider } = makeProvider(); + await provider.executeToolCall("system", "user", toolSchema, "session-id"); + + expect(capturedBody?.model).toBe(CHEAPER_INFERENCE_DEFAULT_MODEL); + expect(capturedBody?.tool_choice).toBe("required"); + expect(Array.isArray(capturedBody?.messages)).toBe(true); + expect(Array.isArray(capturedBody?.tools)).toBe(true); + }); + + it("extracts tool input from an OpenAI-format response", async () => { + globalThis.fetch = (async () => + ({ + ok: true, + status: 200, + json: async () => ({ + choices: [ + { + message: { + content: "", + tool_calls: [ + { + id: "call_1", + type: "function", + function: { + name: "save_memories", + arguments: JSON.stringify({ memory: "captured fact" }), + }, + }, + ], + }, + finish_reason: "tool_calls", + }, + ], + }), + }) as Response) as typeof fetch; + + const { provider } = makeProvider(); + const result = await provider.executeToolCall("system", "user", toolSchema, "session-id"); + + expect(result.success).toBe(true); + expect((result.data as any).memory).toBe("captured fact"); + }); +}); + +describe("AIProviderFactory cheaper-inference wiring", () => { + it("creates an Cheaper Inference provider and lists it as supported", () => { + const provider = AIProviderFactory.createProvider("cheaper-inference", { + model: "", + apiUrl: "", + apiKey: "ci-test-key", + }); + expect(provider).toBeInstanceOf(CheaperInferenceProvider); + expect(provider.getProviderName()).toBe("cheaper-inference"); + expect(AIProviderFactory.getSupportedProviders()).toContain("cheaper-inference"); + }); +}); diff --git a/tests/config-resolution.test.ts b/tests/config-resolution.test.ts index e5b92ddf..a0699642 100644 --- a/tests/config-resolution.test.ts +++ b/tests/config-resolution.test.ts @@ -363,6 +363,60 @@ describe("project-scoped config resolution", () => { /Project config cannot set remote provider fields: memoryProvider/ ); }); + + it("resolves Cheaper Inference API key from CHEAPER_INFERENCE_API_KEY and marks manual ready", () => { + const originalApiKey = process.env.CHEAPER_INFERENCE_API_KEY; + process.env.CHEAPER_INFERENCE_API_KEY = "ci-test-key"; + + try { + existsSpy = spyOn(fs, "existsSync").mockReturnValue(true); + readSpy = spyOn(fs, "readFileSync").mockImplementation((p) => { + const path = normalizePath(p); + if (path.includes(".opencode/opencode-mem")) { + return JSON.stringify({ autoCaptureEnabled: true }) as any; + } + return JSON.stringify({ memoryProvider: "cheaper-inference" }) as any; + }); + + initConfig("/my/project"); + + expect(CONFIG.memoryProvider).toBe("cheaper-inference"); + expect(CONFIG.memoryModel).toBeUndefined(); + expect(CONFIG.memoryApiUrl).toBeUndefined(); + expect(CONFIG.memoryApiKey).toBe("ci-test-key"); + expect(CONFIG.autoCaptureProviderStatus).toEqual({ + ready: true, + mode: "manual", + issues: [], + }); + } finally { + if (originalApiKey === undefined) { + delete process.env.CHEAPER_INFERENCE_API_KEY; + } else { + process.env.CHEAPER_INFERENCE_API_KEY = originalApiKey; + } + } + }); + + it("refuses a project config that switches memoryProvider to Cheaper Inference", () => { + existsSpy = spyOn(fs, "existsSync").mockReturnValue(true); + readSpy = spyOn(fs, "readFileSync").mockImplementation((p) => { + const path = normalizePath(p); + if (path.includes(".opencode/opencode-mem")) { + return JSON.stringify({ memoryProvider: "cheaper-inference" }) as any; + } + return JSON.stringify({ + memoryProvider: "openai-chat", + memoryModel: "gpt-global", + memoryApiUrl: "https://api.openai.com/v1", + memoryApiKey: "global-openai-secret", + }) as any; + }); + + expect(() => initConfig("/my/project")).toThrow( + /Project config cannot set remote provider fields: memoryProvider/ + ); + }); }); describe("structured-output timeout derivation", () => { From 1ef10611a537970bdcd8c9e213987c057426c09f Mon Sep 17 00:00:00 2001 From: EyJunge1 <149941075+EyJunge1@users.noreply.github.com> Date: Wed, 21 Oct 2026 02:17:10 +0200 Subject: [PATCH 2/2] fix(test): correct cheaper-inference factory test wording --- tests/cheaper-inference-provider.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cheaper-inference-provider.test.ts b/tests/cheaper-inference-provider.test.ts index 710ed947..bf6dc5ce 100644 --- a/tests/cheaper-inference-provider.test.ts +++ b/tests/cheaper-inference-provider.test.ts @@ -193,7 +193,7 @@ describe("CheaperInferenceProvider", () => { }); describe("AIProviderFactory cheaper-inference wiring", () => { - it("creates an Cheaper Inference provider and lists it as supported", () => { + it("creates a Cheaper Inference provider and lists it as supported", () => { const provider = AIProviderFactory.createProvider("cheaper-inference", { model: "", apiUrl: "",