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
6 changes: 6 additions & 0 deletions .changeset/opencode-go-session-header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@pymodel/pythinker-code": patch
"@pymodel/pythinker-desktop": patch
---

Fix OpenCode Go requests failing with "Request is missing x-opencode-session".
7 changes: 5 additions & 2 deletions packages/agent-core-v2/src/kosong/model/modelRequesterImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type {
import { translateProviderError } from '#/kosong/protocol/errors';
import type { IProtocolAdapterRegistry } from '#/kosong/protocol/protocol';

import { opencodeSessionHeaders } from './opencodeSession';
import { opencodeSessionHeaders } from '#/llm-adapter/model/opencode-session';

import type { AuthProvider, Model } from './catalog';
import type {
Expand Down Expand Up @@ -92,7 +92,10 @@ export class ModelRequesterImpl implements ModelRequester {
const options: GenerateOptions = {
signal,
cacheKey: params?.cacheKey,
extraHeaders: opencodeSessionHeaders(this.model.baseUrl, params?.conversationId),
extraHeaders: opencodeSessionHeaders(
this.model.baseUrl,
params?.conversationId ?? params?.cacheKey,
),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
sampling: params?.sampling,
thinking:
params?.thinkingEffort === undefined
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { randomUUID } from 'node:crypto';

import { parsePythinkerCodeCustomHeaders } from '@pymodel/pythinker-code-oauth';

import { Disposable } from '#/_base/di/lifecycle';
Expand Down Expand Up @@ -171,7 +173,7 @@ export class ModelCatalog extends Disposable implements IModelCatalog {
messages: [{ role: 'user', content: [{ type: 'text', text: 'ping' }], toolCalls: [] }],
},
undefined,
{ maxCompletionTokens: 512 },
{ cacheKey: randomUUID(), maxCompletionTokens: 512 },
)) {
if (event.type === 'part' && event.part.type === 'text') {
text += event.part.text;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
type LlmRequestContent,
type LlmRequestEvent,
type LlmRequester,
mergeRequestHeaders,
} from '#human/llm/requester/requester';
import type { TokenUsage } from '#human/llm/usage';

Expand All @@ -32,6 +33,7 @@ import { fromLlmAssistantMessage, toLlmMessage, type Tool } from '../contract/me
import { mergeUsagePatch } from '#human/llm/usage';

import type { Model } from './catalog';
import { opencodeSessionHeaders } from './opencode-session';
import type {
ModelRequestEvent,
ModelRequestInput,
Expand Down Expand Up @@ -159,8 +161,23 @@ export class ModelRequesterImpl implements ModelRequester {
};

const credential = await this.model.credentialProvider?.resolve();
const credentialed = applyCredential(resolved.model, credential);
const sessionHeaders = opencodeSessionHeaders(
credentialed.baseUrl,
params?.cacheKey,
credentialed.defaultHeaders,
);
await requester.generate(
{ ...config, model: applyCredential(resolved.model, credential) },
{
...config,
model:
sessionHeaders === undefined
? credentialed
: {
...credentialed,
defaultHeaders: mergeRequestHeaders(credentialed.defaultHeaders, sessionHeaders),
},
},
content,
{
signal: signal ?? new AbortController().signal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ export function isOpencodeGatewayBaseUrl(baseUrl: string | undefined): boolean {
export function opencodeSessionHeaders(
baseUrl: string | undefined,
conversationId: string | undefined,
existing?: Readonly<Record<string, string>>,
): Record<string, string> | undefined {
const id = conversationId?.trim();
if (!isOpencodeGatewayBaseUrl(baseUrl) || id === undefined || id.length === 0) return undefined;
if (Object.keys(existing ?? {}).some((key) => key.toLowerCase() === OPENCODE_SESSION_HEADER)) {
return undefined;
}
return { [OPENCODE_SESSION_HEADER]: id };
}
Original file line number Diff line number Diff line change
Expand Up @@ -467,3 +467,42 @@ describe('buildStreamTiming', () => {
});
});
});

describe('ModelRequesterImpl OpenCode session header', () => {
async function headersFor(
baseUrl: string,
cacheKey: string | undefined,
defaultHeaders?: Record<string, string>,
) {
const requester = new FakeLlmRequester();
requester.handler = (_i, emit) => textStream(emit);
const impl = new ModelRequesterImpl(modelWith(undefined), {
resolve: () => ({
requester,
protocol: 'openai',
model: { ...BASE_LLM_MODEL, baseUrl, defaultHeaders },
}),
});
await collect(impl.request(INPUT, undefined, { cacheKey }));
return requester.calls[0]!.config.model.defaultHeaders;
}

it('sends the session id as x-opencode-session to the OpenCode gateway', async () => {
expect(await headersFor('https://opencode.ai/zen/go/v1', 'session-1')).toEqual({
'x-opencode-session': 'session-1',
});
});

it('does not send the header to other providers or without a session id', async () => {
expect(await headersFor('https://api.example.test/v1', 'session-1')).toBeUndefined();
expect(await headersFor('https://opencode.ai/zen/go/v1', undefined)).toBeUndefined();
});

it('keeps a session header the user configured', async () => {
expect(
await headersFor('https://opencode.ai/zen/go/v1', 'session-1', {
'X-OpenCode-Session': 'custom',
}),
).toEqual({ 'X-OpenCode-Session': 'custom' });
});
});
Loading