Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b7007a7
feat(copilot): copy openai-compatible chat model from upstream
SteffenDE Jan 16, 2026
fc4cf9f
feat(copilot): move responses API to copilot folder
SteffenDE Jan 16, 2026
3a9a9b0
fix(copilot): adapt imports for opencode structure
SteffenDE Jan 16, 2026
f2a1724
feat(copilot): add copilot_cache_control to message conversion
SteffenDE Jan 16, 2026
e33a218
feat(copilot): add reasoning_text/reasoning_opaque schema support
SteffenDE Jan 16, 2026
18ecee3
feat(copilot): add reasoning to message conversion and copilot provid…
SteffenDE Jan 16, 2026
d69ece6
feat(copilot): use local chat model with Copilot-specific changes
SteffenDE Jan 16, 2026
a7ff595
fix(copilot): throw error on multiple reasoning_opaque values
SteffenDE Jan 16, 2026
a47d16c
refactor(copilot): simplify to only handle reasoning_text
SteffenDE Jan 16, 2026
82ca0a3
fix(copilot): use copilot key for provider options
SteffenDE Jan 16, 2026
1599503
fix: adjust provider mapping test for copilot
SteffenDE Jan 20, 2026
adda537
refactor(copilot): move copilot_cache_control to transform.ts
SteffenDE Jan 23, 2026
36ecf33
feat(copilot): add thinking_budget for anthropic models
SteffenDE Jan 23, 2026
aa8838b
Merge branch 'dev' into sd-copilot-provider
SteffenDE Jan 24, 2026
a5a93a1
Merge branch 'dev' into sd-copilot-provider
rekram1-node Jan 30, 2026
2c602c0
fix: id issue
rekram1-node Jan 31, 2026
f3123df
update variants
rekram1-node Jan 31, 2026
5fa23c5
Merge branch 'dev' into sd-copilot-provider
rekram1-node Jan 31, 2026
30dfe22
fix: dont use maxOutputTokens for github copilot provider
rekram1-node Jan 31, 2026
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 packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic"
import { createOpenAI } from "@ai-sdk/openai"
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
import { createOpenRouter, type LanguageModelV2 } from "@openrouter/ai-sdk-provider"
import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/openai-compatible/src"
import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/copilot"
import { createXai } from "@ai-sdk/xai"
import { createMistral } from "@ai-sdk/mistral"
import { createGroq } from "@ai-sdk/groq"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import {
type LanguageModelV2Prompt,
type SharedV2ProviderMetadata,
UnsupportedFunctionalityError,
} from '@ai-sdk/provider';
import type { OpenAICompatibleChatPrompt } from './openai-compatible-api-types';
import { convertToBase64 } from '@ai-sdk/provider-utils';

function getOpenAIMetadata(message: {
providerOptions?: SharedV2ProviderMetadata;
}) {
return message?.providerOptions?.copilot ?? {};
}

export function convertToOpenAICompatibleChatMessages(
prompt: LanguageModelV2Prompt,
): OpenAICompatibleChatPrompt {
const messages: OpenAICompatibleChatPrompt = [];
for (const { role, content, ...message } of prompt) {
const metadata = getOpenAIMetadata({ ...message });
switch (role) {
case 'system': {
messages.push({
role: 'system',
content: [
{
type: 'text',
text: content,
},
],
...metadata,
});
break;
}

case 'user': {
if (content.length === 1 && content[0].type === 'text') {
messages.push({
role: 'user',
content: content[0].text,
...getOpenAIMetadata(content[0]),
});
break;
}

messages.push({
role: 'user',
content: content.map(part => {
const partMetadata = getOpenAIMetadata(part);
switch (part.type) {
case 'text': {
return { type: 'text', text: part.text, ...partMetadata };
}
case 'file': {
if (part.mediaType.startsWith('image/')) {
const mediaType =
part.mediaType === 'image/*'
? 'image/jpeg'
: part.mediaType;

return {
type: 'image_url',
image_url: {
url:
part.data instanceof URL
? part.data.toString()
: `data:${mediaType};base64,${convertToBase64(part.data)}`,
},
...partMetadata,
};
} else {
throw new UnsupportedFunctionalityError({
functionality: `file part media type ${part.mediaType}`,
});
}
}
}
}),
...metadata,
});

break;
}

case 'assistant': {
let text = '';
let reasoningText: string | undefined;
let reasoningOpaque: string | undefined;
const toolCalls: Array<{
id: string;
type: 'function';
function: { name: string; arguments: string };
}> = [];

for (const part of content) {
const partMetadata = getOpenAIMetadata(part);
// Check for reasoningOpaque on any part (may be attached to text/tool-call)
const partOpaque = (
part.providerOptions as { copilot?: { reasoningOpaque?: string } }
)?.copilot?.reasoningOpaque;
if (partOpaque && !reasoningOpaque) {
reasoningOpaque = partOpaque;
}

switch (part.type) {
case 'text': {
text += part.text;
break;
}
case 'reasoning': {
reasoningText = part.text;
break;
}
case 'tool-call': {
toolCalls.push({
id: part.toolCallId,
type: 'function',
function: {
name: part.toolName,
arguments: JSON.stringify(part.input),
},
...partMetadata,
});
break;
}
}
}

messages.push({
role: 'assistant',
content: text || null,
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
reasoning_text: reasoningText,
reasoning_opaque: reasoningOpaque,
...metadata,
});

break;
}

case 'tool': {
for (const toolResponse of content) {
const output = toolResponse.output;

let contentValue: string;
switch (output.type) {
case 'text':
case 'error-text':
contentValue = output.value;
break;
case 'content':
case 'json':
case 'error-json':
contentValue = JSON.stringify(output.value);
break;
}

const toolResponseMetadata = getOpenAIMetadata(toolResponse);
messages.push({
role: 'tool',
tool_call_id: toolResponse.toolCallId,
content: contentValue,
...toolResponseMetadata,
});
}
break;
}

default: {
const _exhaustiveCheck: never = role;
throw new Error(`Unsupported role: ${_exhaustiveCheck}`);
}
}
}

return messages;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function getResponseMetadata({
id,
model,
created,
}: {
id?: string | undefined | null;
created?: number | undefined | null;
model?: string | undefined | null;
}) {
return {
id: id ?? undefined,
modelId: model ?? undefined,
timestamp: created != null ? new Date(created * 1000) : undefined,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { LanguageModelV2FinishReason } from '@ai-sdk/provider';

export function mapOpenAICompatibleFinishReason(
finishReason: string | null | undefined,
): LanguageModelV2FinishReason {
switch (finishReason) {
case 'stop':
return 'stop';
case 'length':
return 'length';
case 'content_filter':
return 'content-filter';
case 'function_call':
case 'tool_calls':
return 'tool-calls';
default:
return 'unknown';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { JSONValue } from '@ai-sdk/provider';

export type OpenAICompatibleChatPrompt = Array<OpenAICompatibleMessage>;

export type OpenAICompatibleMessage =
| OpenAICompatibleSystemMessage
| OpenAICompatibleUserMessage
| OpenAICompatibleAssistantMessage
| OpenAICompatibleToolMessage;

// Allow for arbitrary additional properties for general purpose
// provider-metadata-specific extensibility.
type JsonRecord<T = never> = Record<
string,
JSONValue | JSONValue[] | T | T[] | undefined
>;

export interface OpenAICompatibleSystemMessage
extends JsonRecord<OpenAICompatibleSystemContentPart> {
role: 'system';
content: string | Array<OpenAICompatibleSystemContentPart>;
}

export interface OpenAICompatibleSystemContentPart
extends JsonRecord {
type: 'text';
text: string;
}

export interface OpenAICompatibleUserMessage
extends JsonRecord<OpenAICompatibleContentPart> {
role: 'user';
content: string | Array<OpenAICompatibleContentPart>;
}

export type OpenAICompatibleContentPart =
| OpenAICompatibleContentPartText
| OpenAICompatibleContentPartImage;

export interface OpenAICompatibleContentPartImage extends JsonRecord {
type: 'image_url';
image_url: { url: string };
}

export interface OpenAICompatibleContentPartText extends JsonRecord {
type: 'text';
text: string;
}

export interface OpenAICompatibleAssistantMessage
extends JsonRecord<OpenAICompatibleMessageToolCall> {
role: 'assistant';
content?: string | null;
tool_calls?: Array<OpenAICompatibleMessageToolCall>;
// Copilot-specific reasoning fields
reasoning_text?: string;
reasoning_opaque?: string;
}

export interface OpenAICompatibleMessageToolCall extends JsonRecord {
type: 'function';
id: string;
function: {
arguments: string;
name: string;
};
}

export interface OpenAICompatibleToolMessage
extends JsonRecord {
role: 'tool';
content: string;
tool_call_id: string;
}
Loading
Loading