diff --git a/.github/i18n/README.ar.md b/.github/i18n/README.ar.md index f2529601..2457966a 100644 --- a/.github/i18n/README.ar.md +++ b/.github/i18n/README.ar.md @@ -538,6 +538,12 @@ memory({ mode: "import", inputPath: "./memories.json" }); "memoryApiKey": "env://API_ROUTE_API_KEY", ``` عند اختيار هذا المزود، تُرسل مطالبات الالتقاط التلقائي / الملف، واستجابات النموذج، وسياق المحادثة ذي الصلة إلى `https://global.api-route.com`. +- `runcrate`: إعداد مسبق لـ OpenAI-compatible Chat Completions لـ [Runcrate](https://www.runcrate.ai). `memoryApiUrl` و`memoryModel` اختياريان — الافتراضي `https://api.runcrate.ai/v1` و`deepseek-ai/DeepSeek-V4.1-Flash`. إذا حُذف `memoryApiKey`، يُستخدم `RUNCRATE_API_KEY` من البيئة. مثال: + ```jsonc + "memoryProvider": "runcrate", + "memoryApiKey": "env://RUNCRATE_API_KEY", + ``` + عند اختيار هذا المزود، تُرسل مطالبات الالتقاط التلقائي / الملف، واستجابات النموذج، وسياق المحادثة ذي الصلة إلى `https://api.runcrate.ai`. استكشاف الأخطاء: @@ -545,7 +551,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` / `cheaper-inference` / `api-route`. +- للنماذج التي ترفض استدعاءات الأدوات المفروضة (`tool_choice: "required"`، مثل بعض أوضاع التفكير)، أضف `"forceToolChoice": false` عند استخدام `openai-chat` / `orcarouter` / `atlas-cloud` / `cheaper-inference` / `api-route` / `runcrate`. - لـ `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/.github/i18n/README.de.md b/.github/i18n/README.de.md index f8ce5c2b..54e50466 100644 --- a/.github/i18n/README.de.md +++ b/.github/i18n/README.de.md @@ -539,6 +539,12 @@ Manuelle `memoryProvider`-Modi: "memoryApiKey": "env://API_ROUTE_API_KEY", ``` Wenn dieser Provider gewählt ist, werden Auto-Capture- / Profil-Prompts, Modellantworten und relevanter Gesprächskontext an `https://global.api-route.com` übertragen. +- `runcrate`: OpenAI-kompatibles Chat-Completions-Preset für [Runcrate](https://www.runcrate.ai). `memoryApiUrl` und `memoryModel` sind optional — sie defaulten auf `https://api.runcrate.ai/v1` und `deepseek-ai/DeepSeek-V4.1-Flash`. Fehlt `memoryApiKey`, wird `RUNCRATE_API_KEY` aus der Umgebung verwendet. Beispiel: + ```jsonc + "memoryProvider": "runcrate", + "memoryApiKey": "env://RUNCRATE_API_KEY", + ``` + Wenn dieser Provider gewählt ist, werden Auto-Capture- / Profil-Prompts, Modellantworten und relevanter Gesprächskontext an `https://api.runcrate.ai` übertragen. Fehlerbehebung: @@ -546,7 +552,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` / `cheaper-inference` / `api-route` `"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` / `api-route` / `runcrate` `"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/.github/i18n/README.nl.md b/.github/i18n/README.nl.md index 45f0ddd3..3a0a7b82 100644 --- a/.github/i18n/README.nl.md +++ b/.github/i18n/README.nl.md @@ -540,6 +540,12 @@ Handmatige `memoryProvider`-modi: "memoryApiKey": "env://API_ROUTE_API_KEY", ``` Wanneer deze provider is geselecteerd, worden auto-capture- / profielprompts, modelantwoorden en relevante conversatiecontext verzonden naar `https://global.api-route.com`. +- `runcrate`: OpenAI-compatibel Chat Completions-preset voor [Runcrate](https://www.runcrate.ai). `memoryApiUrl` en `memoryModel` zijn optioneel — ze defaulten naar `https://api.runcrate.ai/v1` en `deepseek-ai/DeepSeek-V4.1-Flash`. Als `memoryApiKey` wordt weggelaten, wordt `RUNCRATE_API_KEY` uit de omgeving gebruikt. Voorbeeld: + ```jsonc + "memoryProvider": "runcrate", + "memoryApiKey": "env://RUNCRATE_API_KEY", + ``` + Wanneer deze provider is geselecteerd, worden auto-capture- / profielprompts, modelantwoorden en relevante conversatiecontext verzonden naar `https://api.runcrate.ai`. Probleemoplossing: @@ -547,7 +553,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` / `cheaper-inference` / `api-route`. +- 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` / `api-route` / `runcrate`. - 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/.github/i18n/README.tr.md b/.github/i18n/README.tr.md index 27281470..24825e4e 100644 --- a/.github/i18n/README.tr.md +++ b/.github/i18n/README.tr.md @@ -540,6 +540,12 @@ Manuel `memoryProvider` modları: "memoryApiKey": "env://API_ROUTE_API_KEY", ``` Bu sağlayıcı seçildiğinde otomatik yakalama / profil prompt’ları, model yanıtları ve ilgili konuşma bağlamı `https://global.api-route.com` adresine iletilir. +- `runcrate`: [Runcrate](https://www.runcrate.ai) için OpenAI uyumlu Chat Completions ön ayarı. `memoryApiUrl` ve `memoryModel` isteğe bağlıdır — varsayılanları `https://api.runcrate.ai/v1` ve `deepseek-ai/DeepSeek-V4.1-Flash`’dir. `memoryApiKey` atlanırsa ortamdan `RUNCRATE_API_KEY` kullanılır. Örnek: + ```jsonc + "memoryProvider": "runcrate", + "memoryApiKey": "env://RUNCRATE_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.runcrate.ai` adresine iletilir. Sorun giderme: @@ -547,7 +553,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` / `cheaper-inference` / `api-route` 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` / `api-route` / `runcrate` 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/.github/i18n/README.zh.md b/.github/i18n/README.zh.md index d90a4e92..38e9e505 100644 --- a/.github/i18n/README.zh.md +++ b/.github/i18n/README.zh.md @@ -534,6 +534,12 @@ memory({ mode: "import", inputPath: "./memories.json" }); "memoryApiKey": "env://API_ROUTE_API_KEY", ``` 选择此提供商时,自动捕获 / 画像提示、模型响应与相关对话上下文会传输到 `https://global.api-route.com`。 +- `runcrate`:面向 [Runcrate](https://www.runcrate.ai) 的 OpenAI 兼容 Chat Completions 预设。`memoryApiUrl` 与 `memoryModel` 可选 — 它们默认为 `https://api.runcrate.ai/v1` 与 `deepseek-ai/DeepSeek-V4.1-Flash`。若省略 `memoryApiKey`,则使用环境中的 `RUNCRATE_API_KEY`。示例: + ```jsonc + "memoryProvider": "runcrate", + "memoryApiKey": "env://RUNCRATE_API_KEY", + ``` + 选择此提供商时,自动捕获 / 画像提示、模型响应与相关对话上下文会传输到 `https://api.runcrate.ai`。 故障排除: @@ -541,7 +547,7 @@ memory({ mode: "import", inputPath: "./memories.json" }); - 若自动捕获报告提供商未连接,请用 `opencode providers list` 确认提供商名称,并先在 opencode 中配置该提供商。 - 若代理或自定义提供商返回纯文本而非结构化/工具输出,请选择另一模型/提供商,或使用上方手动提供商模式之一。 - 对于拒绝 `temperature` 的模型,在使用手动 API 配置时添加 `"memoryTemperature": false`。 -- 对于拒绝强制工具调用(`tool_choice: "required"`,例如某些思考模式)的模型,在使用 `openai-chat` / `orcarouter` / `atlas-cloud` / `cheaper-inference` / `api-route` 时添加 `"forceToolChoice": false`。 +- 对于拒绝强制工具调用(`tool_choice: "required"`,例如某些思考模式)的模型,在使用 `openai-chat` / `orcarouter` / `atlas-cloud` / `cheaper-inference` / `api-route` / `runcrate` 时添加 `"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/README.md b/README.md index 99974438..5b8e90ef 100644 --- a/README.md +++ b/README.md @@ -543,6 +543,12 @@ Manual `memoryProvider` modes: "memoryApiKey": "env://API_ROUTE_API_KEY", ``` When this provider is selected, auto-capture / profile prompts, model responses, and relevant conversation context are transmitted to `https://global.api-route.com`. +- `runcrate`: OpenAI-compatible Chat Completions preset for [Runcrate](https://www.runcrate.ai). `memoryApiUrl` and `memoryModel` are optional — they default to `https://api.runcrate.ai/v1` and `deepseek-ai/DeepSeek-V4.1-Flash`. If `memoryApiKey` is omitted, `RUNCRATE_API_KEY` from the environment is used. Example: + ```jsonc + "memoryProvider": "runcrate", + "memoryApiKey": "env://RUNCRATE_API_KEY", + ``` + When this provider is selected, auto-capture / profile prompts, model responses, and relevant conversation context are transmitted to `https://api.runcrate.ai`. Troubleshooting: @@ -550,7 +556,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` / `cheaper-inference` / `api-route`. +- 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` / `api-route` / `runcrate`. - 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/opencode-mem.example.jsonc b/opencode-mem.example.jsonc index 3fd3a0d0..382c240a 100644 --- a/opencode-mem.example.jsonc +++ b/opencode-mem.example.jsonc @@ -195,7 +195,7 @@ // Faster opencode failover: also lower "opencodeTimeoutMs" (default 90000). // "autoCaptureProviderOrder": "external-first", - // Provider type: "api-route" | "atlas-cloud" | "cheaper-inference" | "openai-chat" | "openai-responses" | "anthropic" | "minimax" | "orcarouter" + // Provider type: "api-route" | "atlas-cloud" | "cheaper-inference" | "openai-chat" | "openai-responses" | "anthropic" | "minimax" | "orcarouter" | "runcrate" // 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", @@ -286,6 +286,15 @@ // // conversation context are sent to global.api-route.com. // // "memoryModel": "gpt-6.1-sol" + // Runcrate (OpenAI-compatible Chat Completions preset, with session support): + // "memoryProvider": "runcrate" + // "memoryApiKey": "env://RUNCRATE_API_KEY" + // // memoryApiUrl and memoryModel are optional — they default to + // // https://api.runcrate.ai/v1 and "deepseek-ai/DeepSeek-V4.1-Flash". + // // When selected, auto-capture/profile prompts, responses, and relevant + // // conversation context are sent to api.runcrate.ai. + // // "memoryModel": "deepseek-ai/DeepSeek-V4.1-Flash" + // Groq (OpenAI-compatible, use openai-chat provider): // "memoryProvider": "openai-chat" // "memoryModel": "llama-3.3-70b-versatile" @@ -314,7 +323,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 / cheaper-inference / api-route (tool_choice: "required"). Default true. + // Force tool calls on openai-chat / orcarouter / atlas-cloud / cheaper-inference / api-route / runcrate (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 be7fe37d..0682a91c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -91,7 +91,8 @@ interface OpenCodeMemConfig { | "openai-responses" | "anthropic" | "minimax" - | "orcarouter"; + | "orcarouter" + | "runcrate"; memoryModel?: string; memoryApiUrl?: string; memoryApiKey?: string; @@ -258,7 +259,8 @@ const DEFAULTS: Required< | "openai-responses" | "anthropic" | "minimax" - | "orcarouter"; + | "orcarouter" + | "runcrate"; memoryTemperature?: number | false; forceToolChoice?: boolean; memoryExtraParams?: Record; @@ -557,7 +559,7 @@ const CONFIG_TEMPLATE = `{ "autoCaptureEnabled": true, - // Provider type: "api-route" | "atlas-cloud" | "cheaper-inference" | "openai-chat" | "openai-responses" | "anthropic" | "minimax" | "orcarouter" + // Provider type: "api-route" | "atlas-cloud" | "cheaper-inference" | "openai-chat" | "openai-responses" | "anthropic" | "minimax" | "orcarouter" | "runcrate" // 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", @@ -648,6 +650,15 @@ const CONFIG_TEMPLATE = `{ // // conversation context are sent to global.api-route.com. // // "memoryModel": "gpt-6.1-sol" + // Runcrate (OpenAI-compatible Chat Completions preset, with session support): + // "memoryProvider": "runcrate" + // "memoryApiKey": "env://RUNCRATE_API_KEY" + // // memoryApiUrl and memoryModel are optional — they default to + // // https://api.runcrate.ai/v1 and "deepseek-ai/DeepSeek-V4.1-Flash". + // // When selected, auto-capture/profile prompts, responses, and relevant + // // conversation context are sent to api.runcrate.ai. + // // "memoryModel": "deepseek-ai/DeepSeek-V4.1-Flash" + // Groq (OpenAI-compatible, use openai-chat provider): // "memoryProvider": "openai-chat" // "memoryModel": "llama-3.3-70b-versatile" @@ -683,7 +694,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 / cheaper-inference / api-route (tool_choice: "required"). Default true. + // Force tool calls on openai-chat / orcarouter / atlas-cloud / cheaper-inference / api-route / runcrate (tool_choice: "required"). Default true. // Some thinking/reasoning models reject forced tool choice — set false to fall back to "auto": // "forceToolChoice": false, @@ -1082,7 +1093,8 @@ function buildConfig(fileConfig: OpenCodeMemConfig) { | "openai-responses" | "anthropic" | "minimax" - | "orcarouter"; + | "orcarouter" + | "runcrate"; const memoryApiKey = resolveSecretValue(fileConfig.memoryApiKey) ?? (memoryProvider === "atlas-cloud" @@ -1091,7 +1103,9 @@ function buildConfig(fileConfig: OpenCodeMemConfig) { ? process.env.CHEAPER_INFERENCE_API_KEY : memoryProvider === "api-route" ? process.env.API_ROUTE_API_KEY - : undefined); + : memoryProvider === "runcrate" + ? process.env.RUNCRATE_API_KEY + : undefined); const embeddingModel = fileConfig.embeddingModel ?? DEFAULTS.embeddingModel; const embeddingPreset = getEmbeddingModelPreset(embeddingModel); const embeddingDimensions = @@ -1333,7 +1347,8 @@ export function getAutoCaptureProviderStatus( config.memoryProvider === "orcarouter" || config.memoryProvider === "atlas-cloud" || config.memoryProvider === "cheaper-inference" || - config.memoryProvider === "api-route" + config.memoryProvider === "api-route" || + config.memoryProvider === "runcrate" ) { if (!hasMemoryApiKey) issues.push("memoryApiKey is not configured"); if (hasPlaceholderMemoryApiKey) issues.push("memoryApiKey contains a placeholder value"); diff --git a/src/services/ai/ai-provider-factory.ts b/src/services/ai/ai-provider-factory.ts index 422cabbf..5e3d6cbb 100644 --- a/src/services/ai/ai-provider-factory.ts +++ b/src/services/ai/ai-provider-factory.ts @@ -8,6 +8,7 @@ import { AtlasCloudProvider } from "./providers/atlas-cloud.js"; import { APIRouteProvider } from "./providers/api-route.js"; import { CheaperInferenceProvider } from "./providers/cheaper-inference.js"; import { OrcaRouterProvider } from "./providers/orcarouter.js"; +import { RuncrateProvider } from "./providers/runcrate.js"; import { aiSessionManager } from "./session/ai-session-manager.js"; import type { AIProviderType } from "./session/session-types.js"; @@ -20,6 +21,9 @@ export class AIProviderFactory { case "api-route": return new APIRouteProvider(config, aiSessionManager); + case "runcrate": + return new RuncrateProvider(config, aiSessionManager); + case "cheaper-inference": return new CheaperInferenceProvider(config, aiSessionManager); @@ -57,6 +61,7 @@ export class AIProviderFactory { "minimax", "google-gemini", "orcarouter", + "runcrate", ]; } diff --git a/src/services/ai/provider-config.ts b/src/services/ai/provider-config.ts index 20b29b72..a324efaf 100644 --- a/src/services/ai/provider-config.ts +++ b/src/services/ai/provider-config.ts @@ -33,7 +33,8 @@ export function buildMemoryProviderConfig( config.memoryProvider === "orcarouter" || config.memoryProvider === "atlas-cloud" || config.memoryProvider === "cheaper-inference" || - config.memoryProvider === "api-route"; + config.memoryProvider === "api-route" || + config.memoryProvider === "runcrate"; if (!memoryModel && !isPresetProvider) issues.push("missing memoryModel"); if (!memoryApiUrl && !isPresetProvider) issues.push("missing memoryApiUrl"); diff --git a/src/services/ai/providers/runcrate.ts b/src/services/ai/providers/runcrate.ts new file mode 100644 index 00000000..84d53581 --- /dev/null +++ b/src/services/ai/providers/runcrate.ts @@ -0,0 +1,67 @@ +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"; + +/** Runcrate OpenAI-compatible endpoint used when `memoryApiUrl` is omitted. */ +export const RUNCRATE_API_URL = "https://api.runcrate.ai/v1"; + +/** + * Default model when `memoryModel` is omitted. Runcrate model IDs are + * `vendor/model` pairs and are sent unchanged (e.g. `deepseek-ai/DeepSeek-V4.1-Flash`). + */ +export const RUNCRATE_DEFAULT_MODEL = "deepseek-ai/DeepSeek-V4.1-Flash"; + +/** + * Runcrate provider. + * + * [Runcrate](https://www.runcrate.ai) exposes an OpenAI-compatible Chat + * Completions API for open models. This provider reuses the OpenAI Chat + * Completions request handling and only overrides the resolved endpoint, + * default model, and the session provider tag, so Runcrate sessions are + * distinguishable in the session store and diagnostics. + * + * Users configure it as: + * "memoryProvider": "runcrate" + * "memoryApiKey": "env://RUNCRATE_API_KEY" + * + * `memoryApiUrl` and `memoryModel` are optional — they default to the Runcrate endpoint and `deepseek-ai/DeepSeek-V4.1-Flash` respectively. + * + * When this provider is selected, auto-capture / profile prompts, model + * responses, and relevant conversation context are transmitted to + * `https://api.runcrate.ai`. + */ +export class RuncrateProvider extends OpenAIChatCompletionProvider { + constructor(config: ProviderConfig, aiSessionManager: AISessionManager) { + super(config, aiSessionManager); + } + + override getProviderName(): string { + return "runcrate"; + } + + protected override sessionProviderTag(): AIProviderType { + return "runcrate"; + } + + /** + * Resolve the OpenAI-compatible endpoint. + * + * Defaults to the Runcrate 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 || RUNCRATE_API_URL; + } + + /** + * Resolve the model ID to send to Runcrate. + * + * Defaults to `deepseek-ai/DeepSeek-V4.1-Flash` when `memoryModel` is not configured. + */ + override resolveModel(): string { + const model = (this.config.model || "").trim(); + return model || RUNCRATE_DEFAULT_MODEL; + } +} diff --git a/src/services/ai/session/session-types.ts b/src/services/ai/session/session-types.ts index f7d607d1..682a4ad0 100644 --- a/src/services/ai/session/session-types.ts +++ b/src/services/ai/session/session-types.ts @@ -7,7 +7,8 @@ export type AIProviderType = | "anthropic" | "minimax" | "google-gemini" - | "orcarouter"; + | "orcarouter" + | "runcrate"; export interface AIMessage { id?: number; diff --git a/src/types/index.ts b/src/types/index.ts index 2d65fd40..c10916c0 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -26,4 +26,5 @@ export type AIProviderType = | "anthropic" | "minimax" | "google-gemini" - | "orcarouter"; + | "orcarouter" + | "runcrate"; diff --git a/tests/ai-provider-config.test.ts b/tests/ai-provider-config.test.ts index e12a40c1..fe9da221 100644 --- a/tests/ai-provider-config.test.ts +++ b/tests/ai-provider-config.test.ts @@ -236,6 +236,30 @@ describe("AI provider config", () => { ).toThrow("missing memoryApiKey"); }); + it("builds runcrate config from only an API key, defaulting model and endpoint", () => { + const providerConfig = buildMemoryProviderConfig({ + memoryProvider: "runcrate", + memoryApiKey: "runcrate-test-key", + }); + + expect(providerConfig).toEqual({ + model: "", + apiUrl: "", + apiKey: "runcrate-test-key", + forceToolChoice: undefined, + maxIterations: undefined, + iterationTimeout: undefined, + }); + }); + + it("still requires an API key for the runcrate provider", () => { + expect(() => + buildMemoryProviderConfig({ + memoryProvider: "runcrate", + }) + ).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/config-resolution.test.ts b/tests/config-resolution.test.ts index 52f4b7be..635e79cb 100644 --- a/tests/config-resolution.test.ts +++ b/tests/config-resolution.test.ts @@ -495,6 +495,60 @@ describe("project-scoped config resolution", () => { /Project config cannot set remote provider fields: memoryProvider/ ); }); + + it("resolves Runcrate API key from RUNCRATE_API_KEY and marks manual ready", () => { + const originalApiKey = process.env.RUNCRATE_API_KEY; + process.env.RUNCRATE_API_KEY = "runcrate-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: "runcrate" }) as any; + }); + + initConfig("/my/project"); + + expect(CONFIG.memoryProvider).toBe("runcrate"); + expect(CONFIG.memoryModel).toBeUndefined(); + expect(CONFIG.memoryApiUrl).toBeUndefined(); + expect(CONFIG.memoryApiKey).toBe("runcrate-test-key"); + expect(CONFIG.autoCaptureProviderStatus).toEqual({ + ready: true, + mode: "manual", + issues: [], + }); + } finally { + if (originalApiKey === undefined) { + delete process.env.RUNCRATE_API_KEY; + } else { + process.env.RUNCRATE_API_KEY = originalApiKey; + } + } + }); + + it("refuses a project config that switches memoryProvider to Runcrate", () => { + 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: "runcrate" }) 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", () => { diff --git a/tests/runcrate-provider.test.ts b/tests/runcrate-provider.test.ts new file mode 100644 index 00000000..bcaccc5c --- /dev/null +++ b/tests/runcrate-provider.test.ts @@ -0,0 +1,206 @@ +import { afterEach, describe, expect, it } from "bun:test"; +import { + RuncrateProvider, + RUNCRATE_API_URL, + RUNCRATE_DEFAULT_MODEL, +} from "../src/services/ai/providers/runcrate.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 RuncrateProvider( + { + model: "", + apiUrl: "", + apiKey: "runcrate-test-key", + ...overrides, + }, + sessionManager as any + ), + sessionManager, + }; +} + +describe("RuncrateProvider", () => { + const originalFetch = globalThis.fetch; + + afterEach(() => { + globalThis.fetch = originalFetch; + }); + + it("reports the runcrate provider name", () => { + const { provider } = makeProvider(); + expect(provider.getProviderName()).toBe("runcrate"); + expect(provider.supportsSession()).toBe(true); + }); + + it("defaults to the Runcrate endpoint when apiUrl is not configured", () => { + const { provider } = makeProvider(); + expect(provider.resolveEndpoint()).toBe(RUNCRATE_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 deepseek-ai/DeepSeek-V4.1-Flash", () => { + const { provider } = makeProvider(); + expect(provider.resolveModel()).toBe(RUNCRATE_DEFAULT_MODEL); + }); + + it("returns a configured model unchanged", () => { + const { provider } = makeProvider({ model: "zai-org/GLM-5.3" }); + expect(provider.resolveModel()).toBe("zai-org/GLM-5.3"); + }); + + it("records the runcrate 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("runcrate"); + }); + + it("targets /chat/completions on Runcrate 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(`${RUNCRATE_API_URL}/chat/completions`); + expect(capturedHeaders?.["Authorization"]).toBe("Bearer runcrate-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(RUNCRATE_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 runcrate wiring", () => { + it("creates a Runcrate provider and lists it as supported", () => { + const provider = AIProviderFactory.createProvider("runcrate", { + model: "", + apiUrl: "", + apiKey: "runcrate-test-key", + }); + expect(provider).toBeInstanceOf(RuncrateProvider); + expect(provider.getProviderName()).toBe("runcrate"); + expect(AIProviderFactory.getSupportedProviders()).toContain("runcrate"); + }); +});