fix: identify OpenCode Go requests by session
This commit is contained in:
@@ -91,6 +91,10 @@ other runtime API.
|
||||
a blocker instead of a raw 500 message.
|
||||
- `call.js` — wire formats and per-provider auth, replicating OpenCode's
|
||||
plugin auth loaders:
|
||||
- OpenCode-hosted providers receive `x-opencode-session`. Session-backed
|
||||
features reuse the real OpenCode session id, walkthrough retries reuse the
|
||||
walkthrough cache key, and standalone one-shot actions receive a fresh
|
||||
opaque id for that generation.
|
||||
- **GitHub Copilot**: fetches the requested model's authenticated `/models`
|
||||
metadata from `https://api.githubcopilot.com` (or
|
||||
`copilot-api.<enterprise>`) and honors its advertised endpoint, preferring
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { readAuthFile, writeAuthFile } from '../opencode/auth.js';
|
||||
import { readConfig, readConfigLayers, isPlainObject } from '../opencode/shared.js';
|
||||
import { getCatalogProvider } from './catalog.js';
|
||||
@@ -645,7 +646,7 @@ export async function resolveProviderLogin({ auth, workingDirectory, providerID
|
||||
|| null;
|
||||
}
|
||||
|
||||
export async function callSmallModel({ auth, catalog, workingDirectory, providerID, modelID, prompt, system, maxOutputTokens, responseSchema, timeoutMs, signal }) {
|
||||
export async function callSmallModel({ auth, catalog, workingDirectory, sessionID, providerID, modelID, prompt, system, maxOutputTokens, responseSchema, timeoutMs, signal }) {
|
||||
const tokens = Number(maxOutputTokens) > 0 ? Number(maxOutputTokens) : DEFAULT_MAX_OUTPUT_TOKENS;
|
||||
const providerConfig = readProviderConfig(workingDirectory, providerID);
|
||||
const runtimeProvider = await getRuntimeProvider(providerID);
|
||||
@@ -795,7 +796,12 @@ export async function callSmallModel({ auth, catalog, workingDirectory, provider
|
||||
baseURL,
|
||||
// Configured headers last: a gateway that authenticates on its own header
|
||||
// must be able to override the bearer default rather than sit beside it.
|
||||
headers: mergeHeadersCaseInsensitive({ Authorization: `Bearer ${apiKey}` }, providerConfig?.headers),
|
||||
headers: mergeHeadersCaseInsensitive(
|
||||
mergeHeadersCaseInsensitive({ Authorization: `Bearer ${apiKey}` }, providerConfig?.headers),
|
||||
providerID.startsWith('opencode')
|
||||
? { 'x-opencode-session': typeof sessionID === 'string' && sessionID.trim() ? sessionID.trim() : randomUUID() }
|
||||
: null,
|
||||
),
|
||||
modelID,
|
||||
prompt,
|
||||
system,
|
||||
|
||||
@@ -483,6 +483,29 @@ describe('callSmallModel — custom provider config', () => {
|
||||
});
|
||||
|
||||
describe('catalog-based base URL (no config override)', () => {
|
||||
it('identifies OpenCode Go requests with the owning conversation', async () => {
|
||||
readConfig.mockReturnValue({});
|
||||
fetchMock.mockResolvedValue(ok('ok'));
|
||||
|
||||
await callSmallModel({
|
||||
auth: { 'opencode-go': { type: 'api', key: 'go-key' } },
|
||||
catalog: {
|
||||
'opencode-go': {
|
||||
id: 'opencode-go',
|
||||
api: 'https://opencode.ai/zen/go/v1',
|
||||
models: { utility: { id: 'utility' } },
|
||||
},
|
||||
},
|
||||
workingDirectory: '/proj',
|
||||
sessionID: 'ses_conversation',
|
||||
providerID: 'opencode-go',
|
||||
modelID: 'utility',
|
||||
prompt: 'hi',
|
||||
});
|
||||
|
||||
expect(lastCall(fetchMock).init.headers['x-opencode-session']).toBe('ses_conversation');
|
||||
});
|
||||
|
||||
it('uses the catalog api field when no config baseURL is set', async () => {
|
||||
readConfig.mockReturnValue({});
|
||||
fetchMock.mockResolvedValue(ok('ok'));
|
||||
|
||||
@@ -102,7 +102,7 @@ const readConfiguredSmallModel = (workingDirectory) => {
|
||||
* Generates text with the user's small model, resolved and authenticated
|
||||
* entirely server-side from the OpenCode config and auth store.
|
||||
*/
|
||||
export async function generateSmallModelText({ prompt, system, maxOutputTokens, model, directory, preferredProviderID, preferredModelID, restrictToPreferredProvider = false, responseSchema, timeoutMs, signal, onOverflow = 'truncate' }) {
|
||||
export async function generateSmallModelText({ prompt, system, maxOutputTokens, model, directory, sessionID, preferredProviderID, preferredModelID, restrictToPreferredProvider = false, responseSchema, timeoutMs, signal, onOverflow = 'truncate' }) {
|
||||
if (typeof prompt !== 'string' || !prompt.trim()) {
|
||||
throw Object.assign(new Error('prompt is required'), { statusCode: 400 });
|
||||
}
|
||||
@@ -169,6 +169,7 @@ export async function generateSmallModelText({ prompt, system, maxOutputTokens,
|
||||
auth,
|
||||
catalog,
|
||||
workingDirectory: directory,
|
||||
sessionID,
|
||||
providerID: resolved.providerID,
|
||||
modelID: resolved.modelID,
|
||||
prompt: clamped.prompt,
|
||||
|
||||
@@ -21,13 +21,14 @@ export function registerSmallModelRoutes(app, { getSmallModelService }) {
|
||||
app.post('/api/small-model/generate', async (req, res) => {
|
||||
try {
|
||||
const { generateSmallModelText } = await getSmallModelService();
|
||||
const { prompt, system, maxOutputTokens, model, directory, preferredProviderID, preferredModelID, restrictToPreferredProvider } = req.body || {};
|
||||
const { prompt, system, maxOutputTokens, model, directory, sessionID, preferredProviderID, preferredModelID, restrictToPreferredProvider } = req.body || {};
|
||||
const result = await generateSmallModelText({
|
||||
prompt,
|
||||
system,
|
||||
maxOutputTokens,
|
||||
model,
|
||||
directory,
|
||||
sessionID,
|
||||
preferredProviderID,
|
||||
preferredModelID,
|
||||
restrictToPreferredProvider: restrictToPreferredProvider === true,
|
||||
|
||||
Reference in New Issue
Block a user