Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/i18n/README.ar.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ memory({ mode: "import", inputPath: "./memories.json" });

إذا عُيِّن `opencodeProvider` و`opencodeModel`، يتقدمان على إعدادات `memoryProvider` اليدوية أدناه.

**اتبع نموذج الجلسة:** عيّن `"opencodeModel": "inherit"` لاستخدام نموذج OpenCode ملموس وقت الاستدعاء بدل معرّف مثبت. لـ **الالتقاط التلقائي**، تُسجَّل كل مطالبة عبر خطاف `chat.params` ويعيد طلب الالتقاط استخدام مزود/نموذج تلك المطالبة. لـ **تعلّم الملف** ومسارات المخرجات المنظمة الأخرى (غير المرتبطة برسالة مستخدم واحدة)، يرجع `inherit` إلى أحدث نموذج في قائمة `model.json` الحديثة في OpenCode (مفضّلًا `opencodeProvider` المُعَد). إرسال معرّف النموذج الحرفي `inherit` غير صالح أبدًا وتسبب سابقًا في `ProviderModelNotFoundError: Model not found: <provider>/inherit` على تلك المسارات. ما زال `opencodeProvider` مطلوبًا كبوابة الإعداد العادية.
**اتبع نموذج الجلسة:** عيّن `"opencodeModel": "inherit"` لاستخدام نموذج OpenCode ملموس وقت الاستدعاء بدل معرّف مثبت. لـ **الالتقاط التلقائي**، تُسجَّل كل مطالبة عبر خطاف `chat.params` ويعيد طلب الالتقاط استخدام مزود/نموذج تلك المطالبة. لـ **تعلّم الملف** ومسارات المخرجات المنظمة الأخرى (غير المرتبطة برسالة مستخدم واحدة)، يعيد `inherit` أولاً استخدام آخر نموذج أبلغ به المضيف عبر `chat.params` / خطاف سياق V2، ثم يرجع إلى قائمة `model.json` الحديثة في OpenCode إن وُجدت (مفضّلًا `opencodeProvider` المُعَد). OpenCode v2 لم يعد يكتب `model.json`، لذا نموذج الجلسة داخل العملية هو الاحتياطي الأساسي هناك. إرسال معرّف النموذج الحرفي `inherit` غير صالح أبدًا وتسبب سابقًا في `ProviderModelNotFoundError: Model not found: <provider>/inherit` على تلك المسارات. ما زال `opencodeProvider` مطلوبًا كبوابة الإعداد العادية.

**الرجوع:** إعداد API يدوي (إن لم تستخدم opencodeProvider):

Expand Down
2 changes: 1 addition & 1 deletion .github/i18n/README.de.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ Unterstützte Provider: jeder von `opencode providers list` gelistete Provider (

Wenn `opencodeProvider` und `opencodeModel` gesetzt sind, haben sie Vorrang vor den manuellen `memoryProvider`-Einstellungen unten.

**Sitzungsmodell folgen:** `"opencodeModel": "inherit"` setzen, um zur Aufrufzeit ein konkretes OpenCode-Modell statt einer gepinnten ID zu nutzen. Für **Auto-Capture** wird jeder Prompt über den `chat.params`-Hook aufgezeichnet und der Capture-Request wiederverwendet Provider/Modell dieses Prompts. Für **Profil-Lernen** und andere Structured-Output-Pfade (die nicht an eine einzelne User-Nachricht gebunden sind) fällt `inherit` auf das neueste Modell in der Recent-Liste von OpenCodes `model.json` zurück (bevorzugt den konfigurierten `opencodeProvider`). Das wörtliche Modell-ID `inherit` zu senden ist nie gültig und verursachte zuvor `ProviderModelNotFoundError: Model not found: <provider>/inherit` auf diesen Pfaden. `opencodeProvider` bleibt als normale Config-Gate erforderlich.
**Sitzungsmodell folgen:** `"opencodeModel": "inherit"` setzen, um zur Aufrufzeit ein konkretes OpenCode-Modell statt einer gepinnten ID zu nutzen. Für **Auto-Capture** wird jeder Prompt über den `chat.params`-Hook aufgezeichnet und der Capture-Request wiederverwendet Provider/Modell dieses Prompts. Für **Profil-Lernen** und andere Structured-Output-Pfade (die nicht an eine einzelne User-Nachricht gebunden sind) nutzt `inherit` zuerst das zuletzt vom Host über `chat.params` / den V2-Context-Hook gemeldete Modell und fällt danach auf die Recent-Liste in OpenCodes `model.json` zurück, falls vorhanden (bevorzugt den konfigurierten `opencodeProvider`). OpenCode v2 schreibt `model.json` nicht mehr; dort ist das In-Process-Sitzungsmodell der primäre Fallback. Das wörtliche Modell-ID `inherit` zu senden ist nie gültig und verursachte zuvor `ProviderModelNotFoundError: Model not found: <provider>/inherit` auf diesen Pfaden. `opencodeProvider` bleibt als normale Config-Gate erforderlich.

**Fallback:** Manuelle API-Konfiguration (wenn opencodeProvider nicht genutzt wird):

Expand Down
2 changes: 1 addition & 1 deletion .github/i18n/README.nl.md
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ Ondersteunde providers: elke provider uit `opencode providers list` (bijv. `anth

Als `opencodeProvider` en `opencodeModel` zijn ingesteld, hebben ze voorrang op de handmatige `memoryProvider`-instellingen hieronder.

**Volg het sessiermodel:** stel `"opencodeModel": "inherit"` in om op call-tijd een concreet OpenCode-model te gebruiken in plaats van een vastgezette id. Voor **auto-capture** wordt elke prompt via de `chat.params`-hook vastgelegd en hergebruikt het capture-verzoek provider/model van die prompt. Voor **profiel-leren** en andere structured-output-paden (die niet aan één gebruikersbericht zijn gebonden) valt `inherit` terug op het meest recente model in OpenCode’s `model.json`-recentelijst (met voorkeur voor de geconfigureerde `opencodeProvider`). Het letterlijke model-id `inherit` versturen is nooit geldig en veroorzaakte eerder `ProviderModelNotFoundError: Model not found: <provider>/inherit` op die paden. `opencodeProvider` blijft vereist als normale configgate.
**Volg het sessiermodel:** stel `"opencodeModel": "inherit"` in om op call-tijd een concreet OpenCode-model te gebruiken in plaats van een vastgezette id. Voor **auto-capture** wordt elke prompt via de `chat.params`-hook vastgelegd en hergebruikt het capture-verzoek provider/model van die prompt. Voor **profiel-leren** en andere structured-output-paden (die niet aan één gebruikersbericht zijn gebonden) hergebruikt `inherit` eerst het laatste model dat de host via `chat.params` / de V2-context-hook meldde, en valt daarna terug op OpenCode’s `model.json`-recentelijst indien aanwezig (met voorkeur voor de geconfigureerde `opencodeProvider`). OpenCode v2 schrijft `model.json` niet meer; daar is het in-process-sessiemodel de primaire fallback. Het letterlijke model-id `inherit` versturen is nooit geldig en veroorzaakte eerder `ProviderModelNotFoundError: Model not found: <provider>/inherit` op die paden. `opencodeProvider` blijft vereist als normale configgate.

**Fallback:** Handmatige API-configuratie (als je geen opencodeProvider gebruikt):

Expand Down
2 changes: 1 addition & 1 deletion .github/i18n/README.tr.md
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ Desteklenen sağlayıcılar: `opencode providers list` tarafından listelenen he

`opencodeProvider` ve `opencodeModel` ayarlanmışsa, aşağıdaki manuel `memoryProvider` ayarlarına göre önceliklidirler.

**Oturum modelini izle:** çağrı anında sabitlenmiş bir kimlik yerine somut bir OpenCode modeli kullanmak için `"opencodeModel": "inherit"` ayarlayın. **Otomatik yakalama** için her prompt `chat.params` kancası üzerinden kaydedilir ve yakalama isteği o prompt’un sağlayıcı/modelini yeniden kullanır. **Profil öğrenme** ve diğer yapılandırılmış çıktı yolları için (tek bir kullanıcı mesajına bağlı değildir), `inherit` OpenCode’un `model.json` son liste içindeki en son modele düşer (yapılandırılmış `opencodeProvider` tercih edilir). Literâl model kimliği `inherit` göndermek asla geçerli değildir ve daha önce bu yollarda `ProviderModelNotFoundError: Model not found: <provider>/inherit`’e neden olmuştur. `opencodeProvider` yine de normal yapılandırma kapısı olarak gereklidir.
**Oturum modelini izle:** çağrı anında sabitlenmiş bir kimlik yerine somut bir OpenCode modeli kullanmak için `"opencodeModel": "inherit"` ayarlayın. **Otomatik yakalama** için her prompt `chat.params` kancası üzerinden kaydedilir ve yakalama isteği o prompt’un sağlayıcı/modelini yeniden kullanır. **Profil öğrenme** ve diğer yapılandırılmış çıktı yolları için (tek bir kullanıcı mesajına bağlı değildir), `inherit` önce host’un `chat.params` / V2 context kancasında bildirdiği son modeli yeniden kullanır; yoksa OpenCode’un `model.json` son listesine düşer (yapılandırılmış `opencodeProvider` tercih edilir). OpenCode v2 artık `model.json` yazmaz; orada birincil geri dönüş süreç içi oturum modelidir. Literâl model kimliği `inherit` göndermek asla geçerli değildir ve daha önce bu yollarda `ProviderModelNotFoundError: Model not found: <provider>/inherit`’e neden olmuştur. `opencodeProvider` yine de normal yapılandırma kapısı olarak gereklidir.

**Yedek:** Manuel API yapılandırması (opencodeProvider kullanılmıyorsa):

Expand Down
2 changes: 1 addition & 1 deletion .github/i18n/README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ memory({ mode: "import", inputPath: "./memories.json" });

若设置了 `opencodeProvider` 与 `opencodeModel`,它们优先于下方的手动 `memoryProvider` 设置。

**跟随会话模型:** 设置 `"opencodeModel": "inherit"` 以在调用时使用具体的 OpenCode 模型,而非固定 id。对于**自动捕获**,每个提示通过 `chat.params` 钩子记录,捕获请求重用该提示的提供商/模型。对于**画像学习**及其他结构化输出路径(不绑定到单条用户消息),`inherit` 回退到 OpenCode `model.json` 最近列表中的最新模型(优先使用已配置的 `opencodeProvider`)。发送字面模型 id `inherit` 永远无效,此前会在这些路径上导致 `ProviderModelNotFoundError: Model not found: <provider>/inherit`。`opencodeProvider` 仍作为正常配置门控是必需的。
**跟随会话模型:** 设置 `"opencodeModel": "inherit"` 以在调用时使用具体的 OpenCode 模型,而非固定 id。对于**自动捕获**,每个提示通过 `chat.params` 钩子记录,捕获请求重用该提示的提供商/模型。对于**画像学习**及其他结构化输出路径(不绑定到单条用户消息),`inherit` 先复用主机在 `chat.params` / V2 context 钩子上报的最近模型,再回退到 OpenCode `model.json` 最近列表(若存在;优先使用已配置的 `opencodeProvider`)。OpenCode v2 不再写入 `model.json`,因此进程内会话模型是主要回退。发送字面模型 id `inherit` 永远无效,此前会在这些路径上导致 `ProviderModelNotFoundError: Model not found: <provider>/inherit`。`opencodeProvider` 仍作为正常配置门控是必需的。

**回退:** 手动 API 配置(若不使用 opencodeProvider):

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ If `autoCaptureEnabled` is `true` (default) but neither `opencodeProvider`/`open

If `opencodeProvider` and `opencodeModel` are set, they take precedence over the manual `memoryProvider` settings below by default (`"autoCaptureProviderOrder": "opencode-first"`). Set `"autoCaptureProviderOrder": "external-first"` to prefer the manual API when both are configured (useful when the host session API is busy/slow); the other channel is used as fallback. Lower `"opencodeTimeoutMs"` to fail over from the opencode channel sooner.

**Follow the session model:** set `"opencodeModel": "inherit"` to use a concrete OpenCode model at call time instead of a pinned id. For **auto-capture**, each prompt is recorded via the `chat.params` hook and the capture request reuses that prompt's provider/model. For **profile learning** and other structured-output paths (which are not tied to a single user message), `inherit` falls back to the most recent model in OpenCode's `model.json` recent list (preferring the configured `opencodeProvider`). Sending the literal model id `inherit` is never valid and previously caused `ProviderModelNotFoundError: Model not found: <provider>/inherit` on those paths. `opencodeProvider` is still required as the normal config gate.
**Follow the session model:** set `"opencodeModel": "inherit"` to use a concrete OpenCode model at call time instead of a pinned id. For **auto-capture**, each prompt is recorded via the `chat.params` hook and the capture request reuses that prompt's provider/model. For **profile learning** and other structured-output paths (which are not tied to a single user message), `inherit` first reuses the last model the host reported on `chat.params` / the V2 context hook, then falls back to OpenCode's `model.json` recent list when present (preferring the configured `opencodeProvider`). OpenCode v2 no longer writes `model.json`, so the in-process session model is the primary fallback there. Sending the literal model id `inherit` is never valid and previously caused `ProviderModelNotFoundError: Model not found: <provider>/inherit` on those paths. `opencodeProvider` is still required as the normal config gate.

**Fallback:** Manual API configuration (if not using opencodeProvider):

Expand Down
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { loadOpencodeProvider } from "./services/ai/opencode-provider-loader.js"
import {
isInternalStructuredSession,
noteStructuredOutputStep,
rememberSessionModel,
STRUCTURED_OUTPUT_AGENT,
STRUCTURED_OUTPUT_MAX_STEPS,
STRUCTURED_OUTPUT_TOOLS,
Expand Down Expand Up @@ -832,6 +833,13 @@ export const OpenCodeMemPlugin: Plugin = async (ctx: PluginInput) => {
"chat.params": async (input, output) => {
applyStructuredOutputChatParams(input, output);

const sessionModel = input?.model;
if (typeof sessionModel?.providerID === "string" && typeof sessionModel?.id === "string") {
// Keep the host-resolved model for inherit on idle paths (profile
// learning, dedup, …). OpenCode v2 no longer writes model.json (#453).
rememberSessionModel(sessionModel.providerID, sessionModel.id);
}

if (!isConfigured() || CONFIG.opencodeModel !== "inherit") return;

try {
Expand Down
30 changes: 27 additions & 3 deletions src/services/ai/opencode-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,12 +377,32 @@ export interface StructuredOutputOptions<T> {
retryCount?: number;
}

/** Last host-reported session model from `chat.params` / the V2 context hook. */
let rememberedSessionModel: { providerID: string; modelID: string } | undefined;

/**
* Remember the concrete model the host is using for the current session.
* OpenCode v2 no longer writes `model.json`, so profile-learning and other
* non-prompt structured-output paths need this in-process fallback for
* `opencodeModel: "inherit"` (issue #453).
*/
export function rememberSessionModel(providerID: string, modelID: string): void {
if (!providerID || !modelID || modelID === "inherit") return;
rememberedSessionModel = { providerID, modelID };
}

/** Test helper: drop the in-process session model between cases. */
export function clearRememberedSessionModelForTests(): void {
rememberedSessionModel = undefined;
}

/**
* Resolve `opencodeModel: "inherit"` to a concrete provider/model.
*
* Prefer an explicit prompt-recorded model (auto-capture path). Otherwise fall
* back to OpenCode's recent model list so profile-learning / conflict / dedup
* paths don't send the literal model id "inherit" (ProviderModelNotFoundError).
* Prefer an explicit prompt-recorded model (auto-capture path). Otherwise use
* the last host-reported session model, then OpenCode's recent `model.json`
* list (v1 hosts), so profile-learning / conflict / dedup paths don't send the
* literal model id "inherit" (ProviderModelNotFoundError).
*/
export function resolveOpencodeModelRef(opts: {
providerID: string;
Expand All @@ -397,6 +417,10 @@ export function resolveOpencodeModelRef(opts: {
return { providerID: opts.prompt.providerId, modelID: opts.prompt.modelId };
}

if (rememberedSessionModel) {
return rememberedSessionModel;
}

const recent = readRecentOpencodeModel(opts.providerID);
if (recent) return recent;

Expand Down
42 changes: 35 additions & 7 deletions tests/opencode-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import {
STRUCTURED_OUTPUT_PERMISSIONS,
STRUCTURED_OUTPUT_TOOLS,
takeStructuredOutputStepAbortError,
rememberSessionModel,
clearRememberedSessionModelForTests,
resolveOpencodeModelRef,
} from "../src/services/ai/opencode-provider.js";

const schema = z.object({
Expand Down Expand Up @@ -1054,9 +1057,11 @@ describe("resolveOpencodeModelRef / inherit", () => {
const { join } = await import("node:path");
tempRoot = mkdtempSync(join(tmpdir(), "opencode-mem-inherit-"));
process.env.XDG_STATE_HOME = tempRoot;
clearRememberedSessionModelForTests();
});

afterEach(async () => {
clearRememberedSessionModelForTests();
if (originalStateHome === undefined) {
delete process.env.XDG_STATE_HOME;
} else {
Expand All @@ -1069,16 +1074,15 @@ describe("resolveOpencodeModelRef / inherit", () => {
}
});

it("passes concrete model ids through unchanged", async () => {
const { resolveOpencodeModelRef } = await import("../src/services/ai/opencode-provider.js");
it("passes concrete model ids through unchanged", () => {
expect(resolveOpencodeModelRef({ providerID: "local", modelID: "h100/qwen:latest" })).toEqual({
providerID: "local",
modelID: "h100/qwen:latest",
});
});

it("prefers the recorded prompt model when modelID is inherit", async () => {
const { resolveOpencodeModelRef } = await import("../src/services/ai/opencode-provider.js");
it("prefers the recorded prompt model when modelID is inherit", () => {
rememberSessionModel("opencode-go", "should-not-win");
expect(
resolveOpencodeModelRef({
providerID: "local",
Expand All @@ -1091,6 +1095,32 @@ describe("resolveOpencodeModelRef / inherit", () => {
});
});

it("uses the remembered host session model before model.json", async () => {
const { mkdirSync, writeFileSync } = await import("node:fs");
const { join } = await import("node:path");
const stateDir = join(tempRoot!, "opencode");
mkdirSync(stateDir, { recursive: true });
writeFileSync(
join(stateDir, "model.json"),
JSON.stringify({
recent: [{ providerID: "local", modelID: "from-model-json" }],
})
);
rememberSessionModel("opencode-go", "host-session-model");
expect(resolveOpencodeModelRef({ providerID: "local", modelID: "inherit" })).toEqual({
providerID: "opencode-go",
modelID: "host-session-model",
});
});

it("ignores blank or literal inherit values when remembering the session model", () => {
rememberSessionModel("opencode-go", "inherit");
rememberSessionModel("", "ignored");
expect(() => resolveOpencodeModelRef({ providerID: "local", modelID: "inherit" })).toThrow(
/no session model was recorded and no recent OpenCode model is available/
);
});

it("falls back to OpenCode recent model.json when inherit has no prompt model", async () => {
const { mkdirSync, writeFileSync } = await import("node:fs");
const { join } = await import("node:path");
Expand All @@ -1106,15 +1136,13 @@ describe("resolveOpencodeModelRef / inherit", () => {
})
);

const { resolveOpencodeModelRef } = await import("../src/services/ai/opencode-provider.js");
expect(resolveOpencodeModelRef({ providerID: "local", modelID: "inherit" })).toEqual({
providerID: "local",
modelID: "h100/qwen:latest",
});
});

it("throws a clear error when inherit has no prompt model and no recent list", async () => {
const { resolveOpencodeModelRef } = await import("../src/services/ai/opencode-provider.js");
it("throws a clear error when inherit has no prompt model and no recent list", () => {
expect(() => resolveOpencodeModelRef({ providerID: "local", modelID: "inherit" })).toThrow(
/no session model was recorded and no recent OpenCode model is available/
);
Expand Down
Loading