From 1c07435099a4128a0e0159865c76fc0d9a2ecb82 Mon Sep 17 00:00:00 2001 From: Guillaume ARM Date: Wed, 10 Jun 2026 00:20:41 +0200 Subject: [PATCH] fix(ai): send model for async opencode prompts --- apis/libai.lua | 31 ++++++++++-- docs/opencode_api.md | 13 +++++ docs/opencode_server_guide.md | 9 +++- manifest.json | 2 +- packages/index.json | 4 +- packages/trapos-ai/ccpm.json | 2 +- packages/trapos/ccpm.json | 2 +- programs/ai.lua | 8 +-- tests/ai.lua | 93 +++++++++++++++++++++++++++++++++++ 9 files changed, 151 insertions(+), 13 deletions(-) diff --git a/apis/libai.lua b/apis/libai.lua index faf7b45..043cd74 100644 --- a/apis/libai.lua +++ b/apis/libai.lua @@ -199,6 +199,15 @@ local function createAi(opts) return DEFAULT_LUA_EXEC_TIMEOUT_SECONDS; end + local function resolveModel(options) + local providerId = options.providerID or settingsLib.get('opencc.provider_id'); + local modelId = options.modelID or settingsLib.get('opencc.model_id'); + if isBlank(providerId) or isBlank(modelId) then + return nil, nil; + end + return providerId, modelId; + end + local function resolveConfig(options) local url = options.serverUrl or settingsLib.get('opencc.server_url'); if not url or url == '' then @@ -206,16 +215,30 @@ local function createAi(opts) end local username = options.username or settingsLib.get('opencc.username') or 'opencode'; local password = options.password or settingsLib.get('opencc.password') or ''; + local providerId, modelId = resolveModel(options); return { url = trimTrailingSlash(url), username = username, password = password, + providerID = providerId, + modelID = modelId, timeoutSeconds = resolveTimeout(options), pollTimeoutSeconds = resolvePollTimeout(options), pollIntervalSeconds = resolvePollInterval(options), }; end + local function buildPromptBody(cfg, messageId, prompt) + local body = { + messageID = messageId, + parts = { { type = 'text', text = prompt } }, + }; + if cfg.providerID and cfg.modelID then + body.model = { providerID = cfg.providerID, modelID = cfg.modelID }; + end + return body; + end + local function createMessageId() local t = math.floor(nowFunc() * 1000); return 'msg_' .. tostring(t) .. '_' .. tostring(math.random(100000, 999999)); @@ -411,10 +434,8 @@ local function createAi(opts) end local messageId = options.messageId or createMessageId(); - local body, code = doPost(cfg, '/session/' .. sessionId .. '/prompt_async', { - messageID = messageId, - parts = { { type = 'text', text = prompt } }, - }); + local body, code = doPost(cfg, '/session/' .. sessionId .. '/prompt_async', + buildPromptBody(cfg, messageId, prompt)); if not body then return false, code; end if code == 404 then return handleMissingSession(persist, sessionSettingKey); @@ -520,6 +541,8 @@ local function createAi(opts) serverUrl = options.serverUrl, username = options.username, password = options.password, + providerID = options.providerID, + modelID = options.modelID, timeoutSeconds = options.timeoutSeconds, }; end diff --git a/docs/opencode_api.md b/docs/opencode_api.md index 561ac6e..15a0409 100644 --- a/docs/opencode_api.md +++ b/docs/opencode_api.md @@ -132,6 +132,19 @@ Abort a running generation. Fire-and-forget variant. Returns `204` immediately. Include `messageID` in the request body to make the assistant response addressable by `GET /session/:id/message/:messageID`. Opencode validates caller-provided message IDs; use IDs starting with `msg`. +**Request body:** +```json +{ + "messageID": "msg_xyz", + "parts": [ + { "type": "text", "text": "your prompt here" } + ], + "model": { "providerID": "anthropic", "modelID": "claude-opus-4-7" } +} +``` + +Unlike `/message`, `model` is **not** optional in practice — omitting it causes the request to be accepted (`204`) without triggering generation, so the assistant message never appears. Always send `providerID` / `modelID`. + `ai` uses this endpoint by default to avoid `504` failures from the blocking `/message` endpoint when the LLM takes longer than one HTTP request timeout. The submitted `messageID` identifies the user message; `ai` polls `GET /session/:id/message` and reads the completed assistant message that follows it. --- diff --git a/docs/opencode_server_guide.md b/docs/opencode_server_guide.md index 9c746cb..f929063 100644 --- a/docs/opencode_server_guide.md +++ b/docs/opencode_server_guide.md @@ -90,6 +90,13 @@ Optional — override the Basic Auth username (default `opencode`): set opencc.username myuser ``` +Pick the provider and model. `ai` posts to `/session/:id/prompt_async`, which (unlike the blocking `/message` endpoint) does **not** fall back to a server-side default — the assistant message will never be generated if these are unset: + +```sh +set opencc.provider_id anthropic +set opencc.model_id claude-opus-4-7 +``` + - **CraftOS-PC (localhost):** `http://127.0.0.1:4096` - **In-game ATM10:** use your LAN IP (e.g. `192.168.x.x`) — add it to `http.rules` in `config/computercraft-server.toml` @@ -124,5 +131,5 @@ Set settings inside the harness before running, or inject them via the test API. | `missing prompt` | No prompt was passed | Run `ai ` or `ai ping` | | `session introuvable; lance: ai new ` | Session was deleted or server restarted | Run `ai new ` | | `erreur message: HTTP 504` | AI took too long | Retry; consider a faster model | -| `delai depasse en attendant la reponse AI` | Async polling timed out | Increase `opencc.poll_timeout_seconds` or check opencode logs | +| `delai depasse en attendant la reponse AI` | Async polling timed out — often because opencode received the prompt but never started generation | Make sure `opencc.provider_id` and `opencc.model_id` are set; otherwise increase `opencc.poll_timeout_seconds` or check opencode logs | | `reponse vide` | Reply had no text parts | Check opencode logs | diff --git a/manifest.json b/manifest.json index 85b22e4..1051c1e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "name": "TrapOS", - "version": "0.8.0", + "version": "0.8.1", "branch": "next", "packages": [ "trapos" diff --git a/packages/index.json b/packages/index.json index 9743772..3cadd5c 100644 --- a/packages/index.json +++ b/packages/index.json @@ -5,8 +5,8 @@ "trapos-boot": "0.3.0", "trapos-net": "0.3.0", "trapos-ui": "0.2.2", - "trapos-ai": "0.6.0", + "trapos-ai": "0.6.1", "trapos-sandbox": "0.1.0", - "trapos": "0.8.0" + "trapos": "0.8.1" } } diff --git a/packages/trapos-ai/ccpm.json b/packages/trapos-ai/ccpm.json index 540308b..84c01c1 100644 --- a/packages/trapos-ai/ccpm.json +++ b/packages/trapos-ai/ccpm.json @@ -1,6 +1,6 @@ { "name": "trapos-ai", - "version": "0.6.0", + "version": "0.6.1", "description": "TrapOS AI client for opencode serve", "dependencies": ["trapos-core"], "files": [ diff --git a/packages/trapos/ccpm.json b/packages/trapos/ccpm.json index dfd0194..04773e2 100644 --- a/packages/trapos/ccpm.json +++ b/packages/trapos/ccpm.json @@ -1,6 +1,6 @@ { "name": "trapos", - "version": "0.8.0", + "version": "0.8.1", "description": "TrapOS full install meta-package", "dependencies": ["trapos-boot", "trapos-net", "trapos-ui", "trapos-test", "trapos-ai"], "files": [], diff --git a/programs/ai.lua b/programs/ai.lua index 250f78d..a606345 100644 --- a/programs/ai.lua +++ b/programs/ai.lua @@ -21,9 +21,11 @@ local function printUsage() print(' opencc.server_url'); print(); print('settings optional:'); - print(' opencc.username (default: opencode)'); - print(' opencc.password (Basic Auth password)'); - print(' opencc.session_id (auto-managed)'); + print(' opencc.username (default: opencode)'); + print(' opencc.password (Basic Auth password)'); + print(' opencc.session_id (auto-managed)'); + print(' opencc.provider_id (e.g. anthropic)'); + print(' opencc.model_id (e.g. claude-opus-4-7)'); print(' opencc.timeout_seconds (per HTTP call, max 60)'); print(' opencc.poll_timeout_seconds (default: 300)'); print(' opencc.poll_interval_seconds (default: 2)'); diff --git a/tests/ai.lua b/tests/ai.lua index 9ed93ed..58c9fb3 100644 --- a/tests/ai.lua +++ b/tests/ai.lua @@ -346,6 +346,99 @@ testlib.test('ask sends exact prompt text', function() testlib.assertEquals(body.parts[1].text, 'my prompt'); end); +testlib.test('ask includes model when provider_id and model_id are set', function() + local httpStub = fakeHttp( + { messageResp('reply') }, + {} + ); + local settingsStub = fakeSettings({ + ['opencc.server_url'] = 'http://host', + ['opencc.session_id'] = 'ses_1', + ['opencc.provider_id'] = 'anthropic', + ['opencc.model_id'] = 'claude-opus-4-7', + }); + local ai = createAi({ http = httpStub, settings = settingsStub }); + + ai.ask('hello'); + + local body = textutils.unserializeJSON(httpStub.postCalls[1].body); + testlib.assertEquals(body.model.providerID, 'anthropic'); + testlib.assertEquals(body.model.modelID, 'claude-opus-4-7'); +end); + +testlib.test('ask omits model when provider_id is missing', function() + local httpStub = fakeHttp( + { messageResp('reply') }, + {} + ); + local settingsStub = fakeSettings({ + ['opencc.server_url'] = 'http://host', + ['opencc.session_id'] = 'ses_1', + ['opencc.model_id'] = 'claude-opus-4-7', + }); + local ai = createAi({ http = httpStub, settings = settingsStub }); + + ai.ask('hello'); + + local body = textutils.unserializeJSON(httpStub.postCalls[1].body); + testlib.assertEquals(body.model, nil); +end); + +testlib.test('ask omits model when model_id is missing', function() + local httpStub = fakeHttp( + { messageResp('reply') }, + {} + ); + local settingsStub = fakeSettings({ + ['opencc.server_url'] = 'http://host', + ['opencc.session_id'] = 'ses_1', + ['opencc.provider_id'] = 'anthropic', + }); + local ai = createAi({ http = httpStub, settings = settingsStub }); + + ai.ask('hello'); + + local body = textutils.unserializeJSON(httpStub.postCalls[1].body); + testlib.assertEquals(body.model, nil); +end); + +testlib.test('ask omits model when neither provider_id nor model_id is set', function() + local httpStub = fakeHttp( + { messageResp('reply') }, + {} + ); + local settingsStub = fakeSettings({ + ['opencc.server_url'] = 'http://host', + ['opencc.session_id'] = 'ses_1', + }); + local ai = createAi({ http = httpStub, settings = settingsStub }); + + ai.ask('hello'); + + local body = textutils.unserializeJSON(httpStub.postCalls[1].body); + testlib.assertEquals(body.model, nil); +end); + +testlib.test('ask options providerID/modelID override settings', function() + local httpStub = fakeHttp( + { messageResp('reply') }, + {} + ); + local settingsStub = fakeSettings({ + ['opencc.server_url'] = 'http://host', + ['opencc.session_id'] = 'ses_1', + ['opencc.provider_id'] = 'anthropic', + ['opencc.model_id'] = 'claude-opus-4-7', + }); + local ai = createAi({ http = httpStub, settings = settingsStub }); + + ai.ask('hello', { providerID = 'openai', modelID = 'gpt-5' }); + + local body = textutils.unserializeJSON(httpStub.postCalls[1].body); + testlib.assertEquals(body.model.providerID, 'openai'); + testlib.assertEquals(body.model.modelID, 'gpt-5'); +end); + testlib.test('ask generates opencode-compatible message ids', function() local httpStub = fakeHttp( { messageResp('reply') },