fix(ai): send model for async opencode prompts
This commit is contained in:
parent
cd6ffc82ff
commit
1c07435099
@ -199,6 +199,15 @@ local function createAi(opts)
|
|||||||
return DEFAULT_LUA_EXEC_TIMEOUT_SECONDS;
|
return DEFAULT_LUA_EXEC_TIMEOUT_SECONDS;
|
||||||
end
|
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 function resolveConfig(options)
|
||||||
local url = options.serverUrl or settingsLib.get('opencc.server_url');
|
local url = options.serverUrl or settingsLib.get('opencc.server_url');
|
||||||
if not url or url == '' then
|
if not url or url == '' then
|
||||||
@ -206,16 +215,30 @@ local function createAi(opts)
|
|||||||
end
|
end
|
||||||
local username = options.username or settingsLib.get('opencc.username') or 'opencode';
|
local username = options.username or settingsLib.get('opencc.username') or 'opencode';
|
||||||
local password = options.password or settingsLib.get('opencc.password') or '';
|
local password = options.password or settingsLib.get('opencc.password') or '';
|
||||||
|
local providerId, modelId = resolveModel(options);
|
||||||
return {
|
return {
|
||||||
url = trimTrailingSlash(url),
|
url = trimTrailingSlash(url),
|
||||||
username = username,
|
username = username,
|
||||||
password = password,
|
password = password,
|
||||||
|
providerID = providerId,
|
||||||
|
modelID = modelId,
|
||||||
timeoutSeconds = resolveTimeout(options),
|
timeoutSeconds = resolveTimeout(options),
|
||||||
pollTimeoutSeconds = resolvePollTimeout(options),
|
pollTimeoutSeconds = resolvePollTimeout(options),
|
||||||
pollIntervalSeconds = resolvePollInterval(options),
|
pollIntervalSeconds = resolvePollInterval(options),
|
||||||
};
|
};
|
||||||
end
|
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 function createMessageId()
|
||||||
local t = math.floor(nowFunc() * 1000);
|
local t = math.floor(nowFunc() * 1000);
|
||||||
return 'msg_' .. tostring(t) .. '_' .. tostring(math.random(100000, 999999));
|
return 'msg_' .. tostring(t) .. '_' .. tostring(math.random(100000, 999999));
|
||||||
@ -411,10 +434,8 @@ local function createAi(opts)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local messageId = options.messageId or createMessageId();
|
local messageId = options.messageId or createMessageId();
|
||||||
local body, code = doPost(cfg, '/session/' .. sessionId .. '/prompt_async', {
|
local body, code = doPost(cfg, '/session/' .. sessionId .. '/prompt_async',
|
||||||
messageID = messageId,
|
buildPromptBody(cfg, messageId, prompt));
|
||||||
parts = { { type = 'text', text = prompt } },
|
|
||||||
});
|
|
||||||
if not body then return false, code; end
|
if not body then return false, code; end
|
||||||
if code == 404 then
|
if code == 404 then
|
||||||
return handleMissingSession(persist, sessionSettingKey);
|
return handleMissingSession(persist, sessionSettingKey);
|
||||||
@ -520,6 +541,8 @@ local function createAi(opts)
|
|||||||
serverUrl = options.serverUrl,
|
serverUrl = options.serverUrl,
|
||||||
username = options.username,
|
username = options.username,
|
||||||
password = options.password,
|
password = options.password,
|
||||||
|
providerID = options.providerID,
|
||||||
|
modelID = options.modelID,
|
||||||
timeoutSeconds = options.timeoutSeconds,
|
timeoutSeconds = options.timeoutSeconds,
|
||||||
};
|
};
|
||||||
end
|
end
|
||||||
|
|||||||
@ -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`.
|
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.
|
`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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -90,6 +90,13 @@ Optional — override the Basic Auth username (default `opencode`):
|
|||||||
set opencc.username myuser
|
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`
|
- **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`
|
- **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 <prompt>` or `ai ping` |
|
| `missing prompt` | No prompt was passed | Run `ai <prompt>` or `ai ping` |
|
||||||
| `session introuvable; lance: ai new <prompt>` | Session was deleted or server restarted | Run `ai new <prompt>` |
|
| `session introuvable; lance: ai new <prompt>` | Session was deleted or server restarted | Run `ai new <prompt>` |
|
||||||
| `erreur message: HTTP 504` | AI took too long | Retry; consider a faster model |
|
| `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 |
|
| `reponse vide` | Reply had no text parts | Check opencode logs |
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "TrapOS",
|
"name": "TrapOS",
|
||||||
"version": "0.8.0",
|
"version": "0.8.1",
|
||||||
"branch": "next",
|
"branch": "next",
|
||||||
"packages": [
|
"packages": [
|
||||||
"trapos"
|
"trapos"
|
||||||
|
|||||||
@ -5,8 +5,8 @@
|
|||||||
"trapos-boot": "0.3.0",
|
"trapos-boot": "0.3.0",
|
||||||
"trapos-net": "0.3.0",
|
"trapos-net": "0.3.0",
|
||||||
"trapos-ui": "0.2.2",
|
"trapos-ui": "0.2.2",
|
||||||
"trapos-ai": "0.6.0",
|
"trapos-ai": "0.6.1",
|
||||||
"trapos-sandbox": "0.1.0",
|
"trapos-sandbox": "0.1.0",
|
||||||
"trapos": "0.8.0"
|
"trapos": "0.8.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trapos-ai",
|
"name": "trapos-ai",
|
||||||
"version": "0.6.0",
|
"version": "0.6.1",
|
||||||
"description": "TrapOS AI client for opencode serve",
|
"description": "TrapOS AI client for opencode serve",
|
||||||
"dependencies": ["trapos-core"],
|
"dependencies": ["trapos-core"],
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trapos",
|
"name": "trapos",
|
||||||
"version": "0.8.0",
|
"version": "0.8.1",
|
||||||
"description": "TrapOS full install meta-package",
|
"description": "TrapOS full install meta-package",
|
||||||
"dependencies": ["trapos-boot", "trapos-net", "trapos-ui", "trapos-test", "trapos-ai"],
|
"dependencies": ["trapos-boot", "trapos-net", "trapos-ui", "trapos-test", "trapos-ai"],
|
||||||
"files": [],
|
"files": [],
|
||||||
|
|||||||
@ -21,9 +21,11 @@ local function printUsage()
|
|||||||
print(' opencc.server_url');
|
print(' opencc.server_url');
|
||||||
print();
|
print();
|
||||||
print('settings optional:');
|
print('settings optional:');
|
||||||
print(' opencc.username (default: opencode)');
|
print(' opencc.username (default: opencode)');
|
||||||
print(' opencc.password (Basic Auth password)');
|
print(' opencc.password (Basic Auth password)');
|
||||||
print(' opencc.session_id (auto-managed)');
|
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.timeout_seconds (per HTTP call, max 60)');
|
||||||
print(' opencc.poll_timeout_seconds (default: 300)');
|
print(' opencc.poll_timeout_seconds (default: 300)');
|
||||||
print(' opencc.poll_interval_seconds (default: 2)');
|
print(' opencc.poll_interval_seconds (default: 2)');
|
||||||
|
|||||||
93
tests/ai.lua
93
tests/ai.lua
@ -346,6 +346,99 @@ testlib.test('ask sends exact prompt text', function()
|
|||||||
testlib.assertEquals(body.parts[1].text, 'my prompt');
|
testlib.assertEquals(body.parts[1].text, 'my prompt');
|
||||||
end);
|
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()
|
testlib.test('ask generates opencode-compatible message ids', function()
|
||||||
local httpStub = fakeHttp(
|
local httpStub = fakeHttp(
|
||||||
{ messageResp('reply') },
|
{ messageResp('reply') },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user