Skip to content
Open
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
7 changes: 5 additions & 2 deletions 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 createCustomOpenAICompatible } from "./sdk/openai-compatible/src"

export namespace Provider {
const log = Log.create({ service: "provider" })
Expand All @@ -37,10 +37,13 @@ export namespace Provider {
"@ai-sdk/google-vertex": createVertex,
"@ai-sdk/google-vertex/anthropic": createVertexAnthropic,
"@ai-sdk/openai": createOpenAI,
// Official OpenAI-compatible provider for standard models
"@ai-sdk/openai-compatible": createOpenAICompatible,
// Custom provider with reasoning_content support for DeepSeek, Qwen, etc.
"@ai-sdk/openai-compatible-reasoning": createCustomOpenAICompatible,
"@openrouter/ai-sdk-provider": createOpenRouter,
// @ts-ignore (TODO: kill this code so we dont have to maintain it)
"@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
"@ai-sdk/github-copilot": createCustomOpenAICompatible,
}

type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
Expand Down
59 changes: 56 additions & 3 deletions packages/opencode/src/provider/sdk/openai-compatible/src/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,58 @@
This is a temporary package used primarily for github copilot compatibility.
# OpenAI-Compatible Provider SDK

Avoid making changes to these files unless you want to only affect Copilot provider.
Custom OpenAI-compatible provider implementations for OpenCode.

Also this should ONLY be used for Copilot provider.
## Purpose

1. **GitHub Copilot** - Provider for GitHub Copilot models
2. **Reasoning Models** - Wrapper that handles `reasoning_content` field from models served via OpenAI-compatible APIs

## Reasoning Provider (`@ai-sdk/openai-compatible-reasoning`)

### What It Does

Detects and transforms `reasoning_content` fields from OpenAI-compatible API responses into proper reasoning events that OpenCode displays as collapsible thinking blocks.

**Important**: This only works with **OpenAI-compatible API endpoints**. Some providers serve models via OpenAI-compatible APIs even if those models have their own native APIs.

### Supported Models

When served via OpenAI-compatible APIs:
- **DeepSeek** (via DeepInfra or DeepSeek's OpenAI-compatible endpoint)
- **Qwen Thinking** (via DeepInfra or other providers)
- **Kimi K2 Thinking** (via providers offering OpenAI-compatible APIs)
- Any model served via OpenAI-compatible APIs that returns `reasoning_content`

### Configuration

```json
{
"provider": {
"deepinfra-thinking": {
"npm": "@ai-sdk/openai-compatible-reasoning",
"options": {
"baseURL": "https://api.deepinfra.com/v1/openai",
"reasoning": { "enabled": true }
},
"models": {
"deepseek-ai/DeepSeek-V3.2": {}
}
}
}
}
```

**Note**: `reasoning.enabled` is required for some models (e.g., DeepSeek) but not others (e.g., Qwen).

## How It Works

Response chunks with `delta.reasoning_content` are transformed into:
- `reasoning-start` → `reasoning-delta` → `reasoning-end` events
- OpenCode's processor displays these as collapsible thinking blocks
- All request handling (including multimodal input) is delegated to the base model

## Files

- `openai-compatible-provider.ts` - Provider factory
- `openai-compatible-chat-reasoning-model.ts` - Reasoning wrapper
- `index.ts` - Exports
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { createOpenaiCompatible, openaiCompatible } from "./openai-compatible-provider"
export type { OpenaiCompatibleProvider, OpenaiCompatibleProviderSettings } from "./openai-compatible-provider"
export { OpenAICompatibleChatWithReasoningLanguageModel } from "./openai-compatible-chat-reasoning-model"
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import type { LanguageModelV2, LanguageModelV2StreamPart } from "@ai-sdk/provider"
import { OpenAICompatibleChatLanguageModel } from "@ai-sdk/openai-compatible"

/**
* Extended OpenAI-compatible chat model that handles reasoning_content field
* in streaming responses (used by DeepSeek, Qwen, and other models).
*
* This wrapper intercepts streaming chunks and transforms chunks with
* `delta.reasoning_content` into proper reasoning-start/delta/end events.
*/
export class OpenAICompatibleChatWithReasoningLanguageModel implements LanguageModelV2 {
readonly specificationVersion = "v2"
readonly provider: string
readonly modelId: string
readonly defaultObjectGenerationMode = "json" as const

private baseModel: OpenAICompatibleChatLanguageModel

constructor(modelId: string, settings: ConstructorParameters<typeof OpenAICompatibleChatLanguageModel>[1]) {
this.baseModel = new OpenAICompatibleChatLanguageModel(modelId, settings)
this.provider = this.baseModel.provider
this.modelId = this.baseModel.modelId
}

get supportedUrls() {
return this.baseModel.supportedUrls
}

async doGenerate(options: Parameters<LanguageModelV2["doGenerate"]>[0]) {
return this.baseModel.doGenerate(options)
}

async doStream(
options: Parameters<LanguageModelV2["doStream"]>[0],
): Promise<Awaited<ReturnType<LanguageModelV2["doStream"]>>> {
// Enable raw chunks so we can see reasoning_content
const modifiedOptions = {
...options,
_internal: {
...(options as any)._internal,
generateId: (options as any)._internal?.generateId,
now: (options as any)._internal?.now,
},
}

const result = await this.baseModel.doStream(modifiedOptions)

// Track reasoning state
let currentReasoningId: string | null = null

// Transform the stream to handle reasoning_content
const transformedStream = result.stream.pipeThrough(
new TransformStream<LanguageModelV2StreamPart, LanguageModelV2StreamPart>({
transform(chunk, controller) {
// Check if this is a raw chunk with reasoning_content
if (chunk.type === "raw") {
try {
const rawValue = chunk.rawValue
// Parse the chunk if it's a string (SSE format)
const parsed = typeof rawValue === "string" ? JSON.parse(rawValue) : rawValue

// Check for reasoning_content in delta
const reasoningContent = parsed?.choices?.[0]?.delta?.reasoning_content
const regularContent = parsed?.choices?.[0]?.delta?.content

if (reasoningContent !== undefined && reasoningContent !== null) {
// We have reasoning content
const reasoningId = currentReasoningId || `reasoning-${Date.now()}`

if (!currentReasoningId) {
// First reasoning chunk - emit reasoning-start
currentReasoningId = reasoningId

controller.enqueue({
type: "reasoning-start",
id: reasoningId,
})
}

// Emit reasoning-delta
controller.enqueue({
type: "reasoning-delta",
id: reasoningId,
delta: reasoningContent,
})
} else if (currentReasoningId && regularContent !== undefined && regularContent !== null) {
// Reasoning has ended, regular content is starting
controller.enqueue({
type: "reasoning-end",
id: currentReasoningId,
})

currentReasoningId = null
}
} catch (e) {
// Failed to parse or process - just pass through
}
}

// Always pass through the original chunk
controller.enqueue(chunk)
},

flush(controller) {
// If reasoning was still active when stream ends, close it
if (currentReasoningId) {
controller.enqueue({
type: "reasoning-end",
id: currentReasoningId,
})
}
},
}),
)

return {
...result,
stream: transformedStream,
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import type { LanguageModelV2 } from "@ai-sdk/provider"
import { OpenAICompatibleChatLanguageModel } from "@ai-sdk/openai-compatible"
import type { LanguageModelV2, EmbeddingModelV2, ImageModelV2 } from "@ai-sdk/provider"
import {
OpenAICompatibleChatLanguageModel,
OpenAICompatibleEmbeddingModel,
OpenAICompatibleImageModel,
} from "@ai-sdk/openai-compatible"
import { type FetchFunction, withoutTrailingSlash, withUserAgentSuffix } from "@ai-sdk/provider-utils"
import { OpenAIResponsesLanguageModel } from "./responses/openai-responses-language-model"
import { OpenAICompatibleChatWithReasoningLanguageModel } from "./openai-compatible-chat-reasoning-model"

// Import the version or define it
const VERSION = "0.1.0"
Expand Down Expand Up @@ -40,10 +45,8 @@ export interface OpenaiCompatibleProvider {
chat(modelId: OpenaiCompatibleModelId): LanguageModelV2
responses(modelId: OpenaiCompatibleModelId): LanguageModelV2
languageModel(modelId: OpenaiCompatibleModelId): LanguageModelV2

// embeddingModel(modelId: any): EmbeddingModelV2

// imageModel(modelId: any): ImageModelV2
textEmbeddingModel(modelId: OpenaiCompatibleModelId): EmbeddingModelV2<string>
imageModel(modelId: OpenaiCompatibleModelId): ImageModelV2
}

/**
Expand All @@ -65,22 +68,28 @@ export function createOpenaiCompatible(options: OpenaiCompatibleProviderSettings

const getHeaders = () => withUserAgentSuffix(headers, `ai-sdk/openai-compatible/${VERSION}`)

// Helper to create common model config
const getCommonModelConfig = (modelType: string) => ({
provider: `${options.name ?? "openai-compatible"}.${modelType}`,
headers: getHeaders,
url: ({ path }: { path: string }) => `${baseURL}${path}`,
fetch: options.fetch,
})

const createChatModel = (modelId: OpenaiCompatibleModelId) => {
return new OpenAICompatibleChatLanguageModel(modelId, {
provider: `${options.name ?? "openai-compatible"}.chat`,
headers: getHeaders,
url: ({ path }) => `${baseURL}${path}`,
fetch: options.fetch,
})
return new OpenAICompatibleChatWithReasoningLanguageModel(modelId, getCommonModelConfig("chat"))
}

const createResponsesModel = (modelId: OpenaiCompatibleModelId) => {
return new OpenAIResponsesLanguageModel(modelId, {
provider: `${options.name ?? "openai-compatible"}.responses`,
headers: getHeaders,
url: ({ path }) => `${baseURL}${path}`,
fetch: options.fetch,
})
return new OpenAIResponsesLanguageModel(modelId, getCommonModelConfig("responses"))
}

const createEmbeddingModel = (modelId: OpenaiCompatibleModelId) => {
return new OpenAICompatibleEmbeddingModel(modelId, getCommonModelConfig("embedding"))
}

const createImageModel = (modelId: OpenaiCompatibleModelId) => {
return new OpenAICompatibleImageModel(modelId, getCommonModelConfig("image"))
}

const createLanguageModel = (modelId: OpenaiCompatibleModelId) => createChatModel(modelId)
Expand All @@ -92,6 +101,8 @@ export function createOpenaiCompatible(options: OpenaiCompatibleProviderSettings
provider.languageModel = createLanguageModel
provider.chat = createChatModel
provider.responses = createResponsesModel
provider.textEmbeddingModel = createEmbeddingModel
provider.imageModel = createImageModel

return provider as OpenaiCompatibleProvider
}
Expand Down
9 changes: 9 additions & 0 deletions packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,15 @@ export namespace ProviderTransform {
): Record<string, any> {
const result: Record<string, any> = {}

// Handle reasoning parameter for openai-compatible-reasoning provider
if (
(model.api.npm === "@ai-sdk/openai-compatible-reasoning" ||
model.api.npm === "@ai-sdk/openai-compatible") &&
providerOptions?.reasoning
) {
result["reasoning"] = providerOptions.reasoning
}

if (model.api.npm === "@openrouter/ai-sdk-provider") {
result["usage"] = {
include: true,
Expand Down
53 changes: 53 additions & 0 deletions packages/web/src/content/docs/models.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,59 @@ Here are several models that work well with OpenCode, in no particular order. (T

---

## Reasoning Models

Some models support extended reasoning capabilities where they show their thinking process before providing the final answer. These "reasoning models" or "thinking models" can provide better results for complex coding tasks by breaking down problems step by step.

### How Reasoning Models Work

Reasoning models separate their output into two parts:

1. **Reasoning/Thinking**: The model's internal thought process (shown in collapsible blocks in the UI)
2. **Response**: The final answer or code

This is similar to how humans solve complex problems - thinking through the solution first, then presenting the final result.

### Built-in Reasoning Models

OpenCode natively supports reasoning from these providers:

- **Anthropic**: Claude Sonnet 4.5 with Extended Thinking
- **OpenAI**: GPT-5, o1, o3 models
- **Google**: Gemini 3 Pro with thinking mode

These models work out of the box - just select them and OpenCode will automatically display their reasoning.

### Models Served via OpenAI-Compatible APIs

Some models return reasoning content when served via **OpenAI-compatible API endpoints**:

- **DeepSeek**: DeepSeek-V3, DeepSeek-R1, deepseek-reasoner (via DeepInfra or DeepSeek's OpenAI-compatible endpoint)
- **Qwen**: Qwen3-235B-A22B-Thinking-2507 (via DeepInfra or other providers)
- **Kimi**: Kimi-K2-Thinking (via providers offering OpenAI-compatible APIs)

To use these models, configure them with the `@ai-sdk/openai-compatible-reasoning` provider. [Learn how to configure reasoning models](/docs/providers#reasoning-models).

:::note[OpenAI-Compatible APIs Only]
This feature requires **OpenAI-compatible API endpoints**. Many providers serve models via OpenAI-compatible APIs even if those models have their own native APIs.
:::

### When to Use Reasoning Models

Reasoning models are particularly useful for:

- Complex architectural decisions
- Debugging difficult issues
- Performance optimization
- Code refactoring with multiple considerations
- Algorithm design and optimization

:::tip
Reasoning models may be slower and more expensive than regular models, but can provide higher quality results for complex tasks.
:::

---

## Set a default

To set one of these as the default model, you can set the `model` key in your
Expand Down
Loading