mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
feat(provider): add OpenCode Go DeepSeek protocol presets / 新增 OpenCode Go DeepSeek 双协议预设 (#8105)
* feat(provider): add OpenCode Go DeepSeek protocol presets Problem: OpenCode Go exposed DeepSeek Flash through Chat Completions, but Reasonix had no dedicated Anthropic Messages or Responses presets with verified server-side web search support. Root cause: The curated catalog mixed unrelated Anthropic models, and Responses web-search replay was restricted to the official DeepSeek hostname instead of the enabled wire capability. Fix: Add model-scoped Anthropic and stateless Responses presets for DeepSeek Flash, enable verified web search, preserve completed Responses search calls for replay, and update Desktop capability rendering, localization, docs, mocks, and live tests. Verification: - go test ./... - focused Desktop provider preset tests - frontend typechecks and startup settings contract - cache impact and cache guard scripts - authenticated OpenCode Go Anthropic and Responses web-search live tests * fix(provider): close OpenCode Go review gaps Problem: Repository lint rejected the protocol preset additions because they grew several baselined owner and test files. The live probes also stopped after the first web-search turn, leaving Responses replay and Anthropic tool-result compatibility unproven. Root cause: OpenCode Go preset data and focused assertions were added inline to already-ratcheted files, while the credential-gated tests did not exercise the provider-issued continuation payloads. Fix: Extract the OpenCode Go preset and localized description data into focused files without changing wire-visible configuration. Add a real Responses stateless follow-up and an Anthropic function tool/result round trip that preserves returned reasoning signatures. Verification: - go test ./... && go vet ./... - go test -race ./internal/config ./internal/provider/responses ./internal/provider/anthropic - go test -tags=live for both OpenCode Go protocol paths - desktop go test ./... && go vet ./... - frontend typecheck, settings contract, build, and bundle budgets - go run ./tools/repolint - ./scripts/cache-guard.sh
This commit is contained in:
1 parent
6376bd4a6d
commit
1cfab35a92
20 files changed
+438
-54
No files matched your search
@@ -6,6 +6,7 @@ import { app, openExternal } from "../lib/bridge";
|
||||
import { normalizeLangPref, useI18n, useT, type DictKey, type LangPref } from "../lib/i18n";
|
||||
import { apiKeyEnvFromProviderName, createLatestRequestGate, inferredVisionModels, mergedFetchedProviderModels, mergeProviderModelContextWindows, providerApiKeyEnvForSave, providerDefaultModel, providerIsConfigured, providerModelCandidates, providerModelContextWindowDrafts, providerModelContextWindowIsSmall, providerRequiresKey } from "../lib/providerModels";
|
||||
import { cachedFetchProviderModels, invalidateProviderCacheByAPIKeyEnv, shouldSkipAutoRefresh } from "../lib/providerModelCache";
|
||||
import { opencodeGoPresetDescriptionKeys } from "../lib/providerPresetDescriptions";
|
||||
import { useUpdater } from "../lib/useUpdater";
|
||||
import {
|
||||
applyTheme,
|
||||
@@ -5289,10 +5290,9 @@ function providerPresetDescription(preset: ProviderPresetView, t: ReturnType<typ
|
||||
return t("settings.addProvider.preset.zaiCodingPlanGlobalDesc");
|
||||
case "zai-coding-plan-global-anthropic":
|
||||
return t("settings.addProvider.preset.zaiCodingPlanGlobalAnthropicDesc");
|
||||
case "opencode-go":
|
||||
return t("settings.addProvider.preset.opencodeGoDesc");
|
||||
case "opencode-go-anthropic":
|
||||
return t("settings.addProvider.preset.opencodeGoAnthropicDesc");
|
||||
case "opencode-go": case "opencode-go-anthropic":
|
||||
case "opencode-go-deepseek-anthropic": case "opencode-go-deepseek-responses":
|
||||
return t(opencodeGoPresetDescriptionKeys[preset.id]);
|
||||
case "opencode-zen-anthropic":
|
||||
return t("settings.addProvider.preset.opencodeZenAnthropicDesc");
|
||||
case "qwen-cn":
|
||||
|
||||
@@ -1192,8 +1192,6 @@ const mockQwenPlanModels = ["qwen3.7-plus", "qwen3.6-plus", "kimi-k2.5", "glm-5"
|
||||
const mockQwenPlanVisionModels = ["qwen3.7-plus", "qwen3.6-plus", "qwen3.5-plus", "kimi-k2.5"];
|
||||
const mockStepFunModels = ["step-3.7-flash", "step-3.5-flash", "step-3.5-flash-2603"];
|
||||
const mockOpenCodeGoModels = ["glm-5.2", "glm-5.1", "kimi-k3", "kimi-k2.7-code", "kimi-k2.6", "deepseek-v4-pro", "deepseek-v4-flash", "mimo-v2.5-pro", "mimo-v2.5"];
|
||||
const mockOpenCodeGoAnthropicModels = ["qwen3.7-plus", "qwen3.7-max", "qwen3.6-plus", "minimax-m3", "minimax-m2.7", "minimax-m2.5"];
|
||||
const mockOpenCodeZenAnthropicModels = ["claude-sonnet-4-6", "claude-opus-4-8", "claude-haiku-4-5", "qwen3.6-plus", "qwen3.5-plus", "qwen3.6-plus-free"];
|
||||
const mockNovitaModels = ["zai-org/glm-5.2", "moonshotai/kimi-k2.7-code", "minimax/minimax-m3", "deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash", "qwen/qwen3.7-max", "qwen/qwen3.6-plus", "zai-org/glm-5v-turbo"];
|
||||
const mockGMIModels = ["zai-org/GLM-5.2-FP8", "deepseek-ai/DeepSeek-V4-Pro", "deepseek-ai/DeepSeek-V4-Flash", "moonshotai/Kimi-K2.7-Code", "anthropic/claude-sonnet-4.6", "openai/gpt-5.5"];
|
||||
const mockVercelModels = ["anthropic/claude-sonnet-4.6", "anthropic/claude-opus-4.8", "openai/gpt-5.4", "openai/gpt-5.4-pro", "moonshotai/kimi-k2.7-code", "zai/glm-5.2", "deepseek/deepseek-v4-pro"];
|
||||
@@ -1226,8 +1224,10 @@ const mockProviderPresetTemplates: MockProviderPresetTemplate[] = [
|
||||
mockPreset("zai-coding-plan-global", "Z.AI Coding Plan Global", "Z.AI international coding-plan endpoint.", "ZAI_CODING_API_KEY", mockProviderTemplate({ name: "zai-coding-plan-global", kind: "openai", baseUrl: "https://api.z.ai/api/coding/paas/v4", models: mockGLMCodingModels, default: "glm-5.2", apiKeyEnv: "ZAI_CODING_API_KEY", contextWindow: 1000000, thinking: "enabled", supportedEfforts: ["enabled", "disabled"], defaultEffort: "enabled" })),
|
||||
mockPreset("zai-coding-plan-global-anthropic", "Z.AI Coding Plan Global Anthropic", "Z.AI international coding-plan Anthropic-compatible endpoint.", "ZAI_CODING_API_KEY", mockProviderTemplate({ name: "zai-coding-plan-global-anthropic", kind: "anthropic", baseUrl: "https://api.z.ai/api/anthropic", models: mockGLMAnthropicModels, default: "glm-5.2", apiKeyEnv: "ZAI_CODING_API_KEY", authHeader: true, thinking: "adaptive", contextWindow: 1000000 })),
|
||||
mockPreset("opencode-go", "OpenCode Go", "OpenCode Go relay with per-model capability overrides.", "OPENCODE_GO_API_KEY", mockProviderTemplate({ name: "opencode-go", kind: "openai", baseUrl: "https://opencode.ai/zen/go/v1", models: mockOpenCodeGoModels, visionModels: ["kimi-k3"], default: "glm-5.2", apiKeyEnv: "OPENCODE_GO_API_KEY", contextWindow: 128000, modelOverrides: [{ model: "kimi-k3", reasoningProtocol: "openai", supportedEfforts: ["high", "max"], defaultEffort: "max", contextWindow: 1048576 }] })),
|
||||
mockPreset("opencode-go-anthropic", "OpenCode Go Anthropic", "OpenCode Go subscription Anthropic-compatible route for Qwen and MiniMax models.", "OPENCODE_GO_API_KEY", mockProviderTemplate({ name: "opencode-go-anthropic", kind: "anthropic", baseUrl: "https://opencode.ai/zen/go", models: mockOpenCodeGoAnthropicModels, visionModels: ["qwen3.7-plus", "qwen3.6-plus"], default: "qwen3.7-plus", apiKeyEnv: "OPENCODE_GO_API_KEY", thinking: "adaptive", contextWindow: 262144 })),
|
||||
mockPreset("opencode-zen-anthropic", "OpenCode Zen Anthropic", "OpenCode Zen Anthropic-compatible route for Claude and Qwen models.", "OPENCODE_API_KEY", mockProviderTemplate({ name: "opencode-zen-anthropic", kind: "anthropic", baseUrl: "https://opencode.ai/zen", models: mockOpenCodeZenAnthropicModels, visionModels: ["claude-sonnet-4-6", "claude-opus-4-8", "claude-haiku-4-5"], default: "claude-sonnet-4-6", apiKeyEnv: "OPENCODE_API_KEY", contextWindow: 262144 })),
|
||||
mockPreset("opencode-go-anthropic", "OpenCode Go Anthropic", "OpenCode Go subscription Anthropic-compatible route for Qwen and MiniMax.", "OPENCODE_GO_API_KEY", mockProviderTemplate({ name: "opencode-go-anthropic", kind: "anthropic", baseUrl: "https://opencode.ai/zen/go", models: ["qwen3.7-plus", "qwen3.7-max", "qwen3.6-plus", "minimax-m3", "minimax-m2.7", "minimax-m2.5"], visionModels: ["qwen3.7-plus", "qwen3.6-plus"], default: "qwen3.7-plus", apiKeyEnv: "OPENCODE_GO_API_KEY", thinking: "adaptive", contextWindow: 262144 })),
|
||||
mockPreset("opencode-go-deepseek-anthropic", "OpenCode Go DeepSeek Anthropic", "OpenCode Go Anthropic Messages route for DeepSeek Flash with server-side web search.", "OPENCODE_GO_API_KEY", mockProviderTemplate({ name: "opencode-go-deepseek-anthropic", kind: "anthropic", baseUrl: "https://opencode.ai/zen/go", models: ["deepseek-v4-flash"], default: "deepseek-v4-flash", apiKeyEnv: "OPENCODE_GO_API_KEY", thinking: "adaptive", webSearch: true, serverWebSearchCapability: true, contextWindow: 262144, supportedEfforts: ["disabled", "high", "max"], defaultEffort: "high" })),
|
||||
mockPreset("opencode-go-deepseek-responses", "OpenCode Go DeepSeek Responses", "OpenCode Go stateless Responses API route for DeepSeek Flash with server-side web search.", "OPENCODE_GO_API_KEY", mockProviderTemplate({ name: "opencode-go-deepseek-responses", kind: "responses", baseUrl: "https://opencode.ai/zen/go/v1", models: ["deepseek-v4-flash"], default: "deepseek-v4-flash", apiKeyEnv: "OPENCODE_GO_API_KEY", webSearch: true, serverWebSearchCapability: true, contextWindow: 128000, supportedEfforts: ["disabled", "high", "max"], defaultEffort: "high" })),
|
||||
mockPreset("opencode-zen-anthropic", "OpenCode Zen Anthropic", "OpenCode Zen Anthropic-compatible route for Claude and Qwen models.", "OPENCODE_API_KEY", mockProviderTemplate({ name: "opencode-zen-anthropic", kind: "anthropic", baseUrl: "https://opencode.ai/zen", models: ["claude-sonnet-4-6", "claude-opus-4-8", "claude-haiku-4-5", "qwen3.6-plus", "qwen3.5-plus", "qwen3.6-plus-free"], visionModels: ["claude-sonnet-4-6", "claude-opus-4-8", "claude-haiku-4-5"], default: "claude-sonnet-4-6", apiKeyEnv: "OPENCODE_API_KEY", contextWindow: 262144 })),
|
||||
mockPreset("qwen-cn", "Qwen CN API", "Alibaba DashScope China standard OpenAI-compatible endpoint.", "QWEN_API_KEY", mockProviderTemplate({ name: "qwen-cn", kind: "openai", baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", models: mockQwenAPIModels, visionModels: ["qwen3.7-plus", "qwen3.6-plus", "qwen3.5-plus", "kimi-k2.5"], default: "qwen3.7-plus", apiKeyEnv: "QWEN_API_KEY" })),
|
||||
mockPreset("qwen-global", "Qwen Global API", "Alibaba DashScope international standard OpenAI-compatible endpoint.", "QWEN_API_KEY", mockProviderTemplate({ name: "qwen-global", kind: "openai", baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", models: mockQwenAPIModels, visionModels: ["qwen3.7-plus", "qwen3.6-plus", "qwen3.5-plus", "kimi-k2.5"], default: "qwen3.7-plus", apiKeyEnv: "QWEN_API_KEY" })),
|
||||
mockPreset("qwen-coding-plan-cn", "Qwen Coding Plan CN", "Alibaba Cloud Qwen Coding Plan China endpoint.", "QWEN_CODING_API_KEY", mockProviderTemplate({ name: "qwen-coding-plan-cn", kind: "openai", baseUrl: "https://coding.dashscope.aliyuncs.com/v1", models: mockQwenPlanModels, visionModels: mockQwenPlanVisionModels, default: "qwen3.7-plus", apiKeyEnv: "QWEN_CODING_API_KEY" })),
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { DictKey } from "./i18n";
|
||||
|
||||
export const opencodeGoPresetDescriptionKeys = {
|
||||
"opencode-go": "settings.addProvider.preset.opencodeGoDesc",
|
||||
"opencode-go-anthropic": "settings.addProvider.preset.opencodeGoAnthropicDesc",
|
||||
"opencode-go-deepseek-anthropic": "settings.addProvider.preset.opencodeGoDeepSeekAnthropicDesc",
|
||||
"opencode-go-deepseek-responses": "settings.addProvider.preset.opencodeGoDeepSeekResponsesDesc",
|
||||
} as const satisfies Record<string, DictKey>;
|
||||
@@ -2569,7 +2569,9 @@ export const en = {
|
||||
"settings.addProvider.preset.zaiCodingPlanGlobalDesc": "Z.AI international coding-plan endpoint with GLM-5.2 default.",
|
||||
"settings.addProvider.preset.zaiCodingPlanGlobalAnthropicDesc": "Z.AI international Anthropic-compatible coding-plan endpoint with GLM-5.2 default.",
|
||||
"settings.addProvider.preset.opencodeGoDesc": "OpenCode Go relay with native Kimi K3 and mixed-model capability overrides.",
|
||||
"settings.addProvider.preset.opencodeGoAnthropicDesc": "OpenCode Go Anthropic route for Qwen and MiniMax models.",
|
||||
"settings.addProvider.preset.opencodeGoAnthropicDesc": "OpenCode Go Anthropic route for Qwen and MiniMax.",
|
||||
"settings.addProvider.preset.opencodeGoDeepSeekAnthropicDesc": "DeepSeek Flash over Anthropic Messages with server-side web search.",
|
||||
"settings.addProvider.preset.opencodeGoDeepSeekResponsesDesc": "DeepSeek Flash over stateless Responses API with server-side web search.",
|
||||
"settings.addProvider.preset.opencodeZenAnthropicDesc": "OpenCode Zen Anthropic route for Claude and Qwen models.",
|
||||
"settings.addProvider.preset.qwenCnDesc": "Qwen/DashScope China standard API endpoint.",
|
||||
"settings.addProvider.preset.qwenGlobalDesc": "Qwen/DashScope international standard API endpoint.",
|
||||
|
||||
@@ -1744,7 +1744,9 @@ export const zhTW: Record<DictKey, string> = {
|
||||
"settings.addProvider.preset.zaiCodingPlanGlobalDesc": "Z.AI 國際 coding plan 端點,預設 GLM-5.2。",
|
||||
"settings.addProvider.preset.zaiCodingPlanGlobalAnthropicDesc": "Z.AI 國際 Anthropic-compatible coding plan 端點,預設 GLM-5.2。",
|
||||
"settings.addProvider.preset.opencodeGoDesc": "OpenCode Go 混合模型閘道,原生預置 Kimi K3 與每模型能力覆蓋。",
|
||||
"settings.addProvider.preset.opencodeGoAnthropicDesc": "OpenCode Go Anthropic 線路,預置 Qwen 與 MiniMax 模型。",
|
||||
"settings.addProvider.preset.opencodeGoAnthropicDesc": "OpenCode Go Anthropic 線路,預置 Qwen 與 MiniMax。",
|
||||
"settings.addProvider.preset.opencodeGoDeepSeekAnthropicDesc": "DeepSeek Flash Anthropic Messages 線路,預設啟用伺服器端網頁搜尋。",
|
||||
"settings.addProvider.preset.opencodeGoDeepSeekResponsesDesc": "DeepSeek Flash 無狀態 Responses API 線路,預設啟用伺服器端網頁搜尋。",
|
||||
"settings.addProvider.preset.opencodeZenAnthropicDesc": "OpenCode Zen Anthropic 線路,預置 Claude 與 Qwen 模型。",
|
||||
"settings.addProvider.preset.qwenCnDesc": "Qwen/DashScope 中國標準 API 端點。",
|
||||
"settings.addProvider.preset.qwenGlobalDesc": "Qwen/DashScope 國際標準 API 端點。",
|
||||
|
||||
@@ -2572,7 +2572,9 @@ export const zh: Record<DictKey, string> = {
|
||||
"settings.addProvider.preset.zaiCodingPlanGlobalDesc": "Z.AI 国际 coding plan 端点,默认 GLM-5.2。",
|
||||
"settings.addProvider.preset.zaiCodingPlanGlobalAnthropicDesc": "Z.AI 国际 Anthropic-compatible coding plan 端点,默认 GLM-5.2。",
|
||||
"settings.addProvider.preset.opencodeGoDesc": "OpenCode Go 混合模型网关,原生预置 Kimi K3 与每模型能力覆盖。",
|
||||
"settings.addProvider.preset.opencodeGoAnthropicDesc": "OpenCode Go Anthropic 线路,预置 Qwen 与 MiniMax 模型。",
|
||||
"settings.addProvider.preset.opencodeGoAnthropicDesc": "OpenCode Go Anthropic 线路,预置 Qwen 与 MiniMax。",
|
||||
"settings.addProvider.preset.opencodeGoDeepSeekAnthropicDesc": "DeepSeek Flash Anthropic Messages 线路,默认启用服务端网页搜索。",
|
||||
"settings.addProvider.preset.opencodeGoDeepSeekResponsesDesc": "DeepSeek Flash 无状态 Responses API 线路,默认启用服务端网页搜索。",
|
||||
"settings.addProvider.preset.opencodeZenAnthropicDesc": "OpenCode Zen Anthropic 线路,预置 Claude 与 Qwen 模型。",
|
||||
"settings.addProvider.preset.qwenCnDesc": "Qwen/DashScope 国内标准 API 端点。",
|
||||
"settings.addProvider.preset.qwenGlobalDesc": "Qwen/DashScope 国际标准 API 端点。",
|
||||
|
||||
@@ -652,7 +652,7 @@ func providerViewFromEntryForRootWithResolverAndCredentials(p config.ProviderEnt
|
||||
ReasoningProtocol: p.ReasoningProtocol,
|
||||
Thinking: providerThinkingForSettings(p.Thinking),
|
||||
WebSearch: config.EffectiveWebSearch(&p),
|
||||
ServerWebSearchCapability: config.IsOfficialDeepSeekWebSearchEndpoint(&p),
|
||||
ServerWebSearchCapability: config.HasServerWebSearchCapability(&p),
|
||||
SupportedEfforts: nonNil(p.SupportedEfforts),
|
||||
DefaultEffort: p.DefaultEffort,
|
||||
ModelOverrides: providerModelOverridesForView(p.ModelOverrides, models),
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/config"
|
||||
)
|
||||
|
||||
func TestProviderViewFromEntryUsesOpenCodeGoDeepSeekWebSearch(t *testing.T) {
|
||||
enabled := true
|
||||
view := providerViewFromEntry(config.ProviderEntry{
|
||||
Name: "opencode-go-deepseek-responses",
|
||||
Kind: "responses",
|
||||
BaseURL: "https://opencode.ai/zen/go/v1",
|
||||
Models: []string{"deepseek-v4-flash"},
|
||||
WebSearch: &enabled,
|
||||
}, false, true)
|
||||
if !view.ServerWebSearchCapability || !view.WebSearch {
|
||||
t.Fatalf("OpenCode Go DeepSeek web search view = %+v, want enabled verified capability", view)
|
||||
}
|
||||
}
|
||||
+11
-3
@@ -390,7 +390,8 @@ Kimi Global,
|
||||
Kimi Coding Plan, MiMo API, MiMo Anthropic, MiMo Token Plan CN/SGP/AMS and their
|
||||
Anthropic-compatible variants, MiniMax CN/Global API, MiniMax CN/Global
|
||||
Anthropic, GLM CN, Z.AI Global, GLM/Z.AI Coding Plan OpenAI-compatible and
|
||||
Anthropic-compatible endpoints, OpenCode Go, OpenCode Go Anthropic, OpenCode Zen
|
||||
Anthropic-compatible endpoints, OpenCode Go, OpenCode Go Anthropic, OpenCode Go
|
||||
DeepSeek Anthropic, OpenCode Go DeepSeek Responses, OpenCode Zen
|
||||
Anthropic, Qwen/DashScope CN/Global, Qwen Coding Plan CN/Global
|
||||
OpenAI-compatible and Anthropic-compatible endpoints, StepFun OpenAI-compatible
|
||||
and Anthropic-compatible endpoints, NovitaAI, GMI Cloud, Vercel AI Gateway,
|
||||
@@ -403,8 +404,15 @@ usually needs only the provider API key: the key value is stored in Reasonix hom
|
||||
environment-variable name, context window, vision model metadata, proxy bypass
|
||||
for China-only endpoints, MiniMax `reasoning_split`, GLM/MiniMax thinking
|
||||
heuristics, Anthropic-compatible Bearer auth where needed, Ollama Cloud
|
||||
max-effort support, and OpenCode Go per-model reasoning overrides. The OpenCode
|
||||
Go preset includes its native `kimi-k3` subscription route with image input,
|
||||
max-effort support, and OpenCode Go per-model reasoning overrides. The dedicated
|
||||
OpenCode Go DeepSeek Anthropic and DeepSeek Responses presets expose the verified
|
||||
Flash routes and enable provider-side `web_search` by default; the Responses
|
||||
variant uses stateless context replay. The existing mixed OpenCode Go Anthropic
|
||||
preset remains scoped to Qwen and MiniMax so server tools are not sent to
|
||||
unverified models. DeepSeek Pro remains on the Chat Completions preset because
|
||||
live Anthropic and Responses requests currently fail in the OpenCode Go upstream
|
||||
conversion. The OpenCode Go preset includes its native `kimi-k3` subscription
|
||||
route with image input,
|
||||
`high`/`max` reasoning effort, and a 1,048,576-token context window. Existing untouched
|
||||
OpenCode Go preset installs are upgraded automatically; edited model catalogs
|
||||
are preserved. The Kimi CN and Kimi Global direct-API presets also include
|
||||
|
||||
+10
-3
@@ -334,7 +334,9 @@ Kimi CN、Kimi Global、Kimi Coding Plan、MiMo API、MiMo Anthropic、MiMo Toke
|
||||
CN/SGP/AMS 及其 Anthropic-compatible 变体、MiniMax CN/Global API、MiniMax
|
||||
CN/Global Anthropic、GLM CN、Z.AI Global、GLM/Z.AI Coding Plan 的
|
||||
OpenAI-compatible 与 Anthropic-compatible 端点、OpenCode Go、OpenCode Go
|
||||
Anthropic、OpenCode Zen Anthropic、Qwen/DashScope CN/Global、Qwen Coding Plan
|
||||
Anthropic、OpenCode Go DeepSeek Anthropic、OpenCode Go DeepSeek Responses、
|
||||
OpenCode Zen Anthropic、Qwen/DashScope CN/Global、
|
||||
Qwen Coding Plan
|
||||
CN/Global 的 OpenAI-compatible 与 Anthropic-compatible 端点、StepFun
|
||||
OpenAI-compatible 与 Anthropic-compatible 端点、NovitaAI、GMI Cloud、Vercel AI
|
||||
Gateway、HuggingFace Router、NVIDIA NIM、KiloCode 和 Ollama Cloud。Plan 表示
|
||||
@@ -344,8 +346,13 @@ Gateway、HuggingFace Router、NVIDIA NIM、KiloCode 和 Ollama Cloud。Plan 表
|
||||
`config.toml` 只保存端点、模型列表、key 环境变量名、上下文窗口、视觉模型元数据、
|
||||
中国区端点直连、MiniMax `reasoning_split`、GLM/MiniMax thinking heuristic、
|
||||
Anthropic-compatible 网关需要的 Bearer 认证、Ollama Cloud max-effort 支持,
|
||||
以及 OpenCode Go 的每模型 reasoning 覆盖。OpenCode Go 预设原生包含订阅线路的
|
||||
`kimi-k3`,并配置图像输入、`high`/`max` 推理强度和 1,048,576 token 上下文窗口。未修改过
|
||||
以及 OpenCode Go 的每模型 reasoning 覆盖。专用的 OpenCode Go DeepSeek Anthropic 与
|
||||
DeepSeek Responses 预设接入已验证的 Flash 线路,并默认启用 provider 侧 `web_search`;
|
||||
Responses 变体使用无状态上下文回放。原有混合 OpenCode Go Anthropic 预设仍只包含 Qwen
|
||||
与 MiniMax,避免把服务端搜索工具发送给未验证模型。DeepSeek Pro 暂时仍只放在 Chat
|
||||
Completions 预设中,因为真实 Anthropic
|
||||
和 Responses 请求目前会在 OpenCode Go 的上游转换阶段失败。OpenCode Go 预设原生包含
|
||||
订阅线路的 `kimi-k3`,并配置图像输入、`high`/`max` 推理强度和 1,048,576 token 上下文窗口。未修改过
|
||||
模型目录的既有 OpenCode Go 预设会自动升级;用户编辑过的模型目录保持不变。
|
||||
Kimi CN 和 Kimi Global 直连 API 预设也包含 `kimi-k3`,支持图像输入、1,048,576 token
|
||||
上下文窗口以及官方 `low`/`high`/`max` 推理强度(默认 `max`)。对官方 K3 端点,Reasonix
|
||||
|
||||
@@ -114,7 +114,6 @@ var (
|
||||
legacyOpenCodeGoModels = []string{"glm-5.2", "glm-5.1", "kimi-k2.7-code", "kimi-k2.6", "deepseek-v4-pro", "deepseek-v4-flash", "mimo-v2.5-pro", "mimo-v2.5"}
|
||||
opencodeGoModels = []string{"glm-5.2", "glm-5.1", "kimi-k3", "kimi-k2.7-code", "kimi-k2.6", "deepseek-v4-pro", "deepseek-v4-flash", "mimo-v2.5-pro", "mimo-v2.5"}
|
||||
opencodeGoVisionModels = []string{"kimi-k3"}
|
||||
opencodeGoAnthropicModels = []string{"qwen3.7-plus", "qwen3.7-max", "qwen3.6-plus", "minimax-m3", "minimax-m2.7", "minimax-m2.5"}
|
||||
opencodeZenAnthropicModels = []string{"claude-sonnet-4-6", "claude-opus-4-8", "claude-haiku-4-5", "qwen3.6-plus", "qwen3.5-plus", "qwen3.6-plus-free"}
|
||||
opencodeZenAnthropicVisionModels = []string{"claude-sonnet-4-6", "claude-opus-4-8", "claude-haiku-4-5"}
|
||||
|
||||
@@ -706,23 +705,9 @@ var curatedProviderPresets = []ProviderPreset{
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
ID: "opencode-go-anthropic",
|
||||
Label: "OpenCode Go Anthropic",
|
||||
Description: "OpenCode Go subscription Anthropic-compatible route for Qwen and MiniMax models.",
|
||||
KeyEnv: "OPENCODE_GO_API_KEY",
|
||||
Entries: []ProviderEntry{{
|
||||
Name: "opencode-go-anthropic",
|
||||
Kind: "anthropic",
|
||||
BaseURL: "https://opencode.ai/zen/go",
|
||||
Models: opencodeGoAnthropicModels,
|
||||
VisionModels: []string{"qwen3.7-plus", "qwen3.6-plus"},
|
||||
Default: "qwen3.7-plus",
|
||||
APIKeyEnv: "OPENCODE_GO_API_KEY",
|
||||
Thinking: "adaptive",
|
||||
ContextWindow: 262144,
|
||||
}},
|
||||
},
|
||||
opencodeGoAnthropicPreset,
|
||||
opencodeGoDeepSeekAnthropicPreset,
|
||||
opencodeGoDeepSeekResponsesPreset,
|
||||
{
|
||||
ID: "opencode-zen-anthropic",
|
||||
Label: "OpenCode Zen Anthropic",
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package config
|
||||
|
||||
var opencodeGoAnthropicPreset = ProviderPreset{
|
||||
ID: "opencode-go-anthropic",
|
||||
Label: "OpenCode Go Anthropic",
|
||||
Description: "OpenCode Go subscription Anthropic-compatible route for Qwen and MiniMax models.",
|
||||
KeyEnv: "OPENCODE_GO_API_KEY",
|
||||
Entries: []ProviderEntry{{
|
||||
Name: "opencode-go-anthropic",
|
||||
Kind: "anthropic",
|
||||
BaseURL: "https://opencode.ai/zen/go",
|
||||
Models: []string{"qwen3.7-plus", "qwen3.7-max", "qwen3.6-plus", "minimax-m3", "minimax-m2.7", "minimax-m2.5"},
|
||||
VisionModels: []string{"qwen3.7-plus", "qwen3.6-plus"},
|
||||
Default: "qwen3.7-plus",
|
||||
APIKeyEnv: "OPENCODE_GO_API_KEY",
|
||||
Thinking: "adaptive",
|
||||
ContextWindow: 262144,
|
||||
}},
|
||||
}
|
||||
|
||||
var opencodeGoDeepSeekAnthropicPreset = ProviderPreset{
|
||||
ID: "opencode-go-deepseek-anthropic",
|
||||
Label: "OpenCode Go DeepSeek Anthropic",
|
||||
Description: "OpenCode Go Anthropic Messages route for DeepSeek Flash with server-side web search.",
|
||||
KeyEnv: "OPENCODE_GO_API_KEY",
|
||||
Entries: []ProviderEntry{{
|
||||
Name: "opencode-go-deepseek-anthropic",
|
||||
Kind: "anthropic",
|
||||
BaseURL: "https://opencode.ai/zen/go",
|
||||
Models: []string{"deepseek-v4-flash"},
|
||||
Default: "deepseek-v4-flash",
|
||||
APIKeyEnv: "OPENCODE_GO_API_KEY",
|
||||
Thinking: "adaptive",
|
||||
WebSearch: boolPointer(true),
|
||||
ContextWindow: 262144,
|
||||
SupportedEfforts: []string{"disabled", "high", "max"},
|
||||
DefaultEffort: "high",
|
||||
}},
|
||||
}
|
||||
|
||||
var opencodeGoDeepSeekResponsesPreset = ProviderPreset{
|
||||
ID: "opencode-go-deepseek-responses",
|
||||
Label: "OpenCode Go DeepSeek Responses",
|
||||
Description: "OpenCode Go stateless Responses API route for DeepSeek Flash with server-side web search.",
|
||||
KeyEnv: "OPENCODE_GO_API_KEY",
|
||||
Entries: []ProviderEntry{{
|
||||
Name: "opencode-go-deepseek-responses",
|
||||
Kind: "responses",
|
||||
BaseURL: "https://opencode.ai/zen/go/v1",
|
||||
Models: []string{"deepseek-v4-flash"},
|
||||
Default: "deepseek-v4-flash",
|
||||
APIKeyEnv: "OPENCODE_GO_API_KEY",
|
||||
ResponsesMode: "stateless",
|
||||
WebSearch: boolPointer(true),
|
||||
ContextWindow: 128000,
|
||||
SupportedEfforts: []string{"disabled", "high", "max"},
|
||||
DefaultEffort: "high",
|
||||
}},
|
||||
}
|
||||
@@ -51,6 +51,8 @@ func TestCuratedProviderPresetsCoverRequestedProviders(t *testing.T) {
|
||||
"zai-coding-plan-global-anthropic",
|
||||
"opencode-go",
|
||||
"opencode-go-anthropic",
|
||||
"opencode-go-deepseek-anthropic",
|
||||
"opencode-go-deepseek-responses",
|
||||
"opencode-zen-anthropic",
|
||||
"qwen-cn",
|
||||
"qwen-global",
|
||||
@@ -97,6 +99,45 @@ func TestCuratedProviderPresetsCoverRequestedProviders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenCodeGoDeepSeekAlternativeProtocolPresets(t *testing.T) {
|
||||
responsesPreset, ok := CuratedProviderPreset("opencode-go-deepseek-responses")
|
||||
if !ok || len(responsesPreset.Entries) != 1 {
|
||||
t.Fatalf("opencode-go-deepseek-responses preset = %+v, found=%v", responsesPreset, ok)
|
||||
}
|
||||
responses := responsesPreset.Entries[0]
|
||||
if responses.Kind != "responses" || responses.BaseURL != "https://opencode.ai/zen/go/v1" || responses.ResponsesMode != "stateless" || responses.Default != "deepseek-v4-flash" {
|
||||
t.Fatalf("opencode-go-deepseek-responses entry = %+v", responses)
|
||||
}
|
||||
if !responses.HasModel("deepseek-v4-flash") || responses.HasModel("deepseek-v4-pro") {
|
||||
t.Fatalf("opencode-go-deepseek-responses models = %v, want currently verified Flash only", responses.ModelList())
|
||||
}
|
||||
if !EffectiveWebSearch(&responses) || !HasServerWebSearchCapability(&responses) {
|
||||
t.Fatalf("opencode-go-deepseek-responses web search = effective:%t capability:%t", EffectiveWebSearch(&responses), HasServerWebSearchCapability(&responses))
|
||||
}
|
||||
if cap := EffortCapabilityForEntry(&responses); !cap.Supported || cap.Default != "high" || !containsString(cap.Levels, "disabled") || !containsString(cap.Levels, "max") {
|
||||
t.Fatalf("opencode-go-deepseek-responses effort capability = %+v", cap)
|
||||
}
|
||||
|
||||
anthropicPreset, ok := CuratedProviderPreset("opencode-go-deepseek-anthropic")
|
||||
if !ok || len(anthropicPreset.Entries) != 1 {
|
||||
t.Fatalf("opencode-go-deepseek-anthropic preset = %+v, found=%v", anthropicPreset, ok)
|
||||
}
|
||||
var cfg Config
|
||||
if err := cfg.UpsertProvider(anthropicPreset.Entries[0]); err != nil {
|
||||
t.Fatalf("UpsertProvider(opencode-go-deepseek-anthropic): %v", err)
|
||||
}
|
||||
flash, ok := cfg.ResolveModel("opencode-go-deepseek-anthropic/deepseek-v4-flash")
|
||||
if !ok {
|
||||
t.Fatal("opencode-go-deepseek-anthropic/deepseek-v4-flash did not resolve")
|
||||
}
|
||||
if cfgEntry, _ := cfg.Provider("opencode-go-deepseek-anthropic"); cfgEntry.HasModel("deepseek-v4-pro") || !EffectiveWebSearch(cfgEntry) || !HasServerWebSearchCapability(cfgEntry) {
|
||||
t.Fatalf("opencode-go-deepseek-anthropic entry = %+v, want Flash-only web search capability", cfgEntry)
|
||||
}
|
||||
if cap := EffortCapabilityForEntry(flash); !cap.Supported || cap.Default != "high" || !containsString(cap.Levels, "max") {
|
||||
t.Fatalf("opencode-go-deepseek-anthropic Flash effort capability = %+v", cap)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeepSeekAnthropicPresetIsOptionalAndModelScoped(t *testing.T) {
|
||||
preset, ok := CuratedProviderPreset("deepseek-anthropic")
|
||||
if !ok || len(preset.Entries) != 1 {
|
||||
@@ -563,7 +604,7 @@ func TestCuratedProviderPresetCapabilities(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatal("opencode-go-anthropic provider missing")
|
||||
}
|
||||
if goAnthropic.Kind != "anthropic" || goAnthropic.BaseURL != "https://opencode.ai/zen/go" || goAnthropic.DefaultModel() != "qwen3.7-plus" || !goAnthropic.HasModel("minimax-m3") {
|
||||
if goAnthropic.Kind != "anthropic" || goAnthropic.BaseURL != "https://opencode.ai/zen/go" || goAnthropic.DefaultModel() != "qwen3.7-plus" || !goAnthropic.HasModel("minimax-m3") || goAnthropic.HasModel("deepseek-v4-flash") {
|
||||
t.Fatalf("opencode-go-anthropic capability mismatch: %+v", goAnthropic)
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,41 @@ func IsOfficialDeepSeekWebSearchEndpoint(e *ProviderEntry) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// IsOpenCodeGoDeepSeekWebSearchEndpoint matches the exact OpenCode Go routes
|
||||
// and model set verified to execute provider-side search. The capability is
|
||||
// model-scoped because the existing Anthropic preset also contains Qwen and
|
||||
// MiniMax models whose server-tool behavior is independent and not implied by
|
||||
// the shared endpoint.
|
||||
func IsOpenCodeGoDeepSeekWebSearchEndpoint(e *ProviderEntry) bool {
|
||||
if !SupportsServerWebSearch(e) {
|
||||
return false
|
||||
}
|
||||
u, err := url.Parse(strings.TrimSpace(e.BaseURL))
|
||||
if err != nil || !strings.EqualFold(u.Scheme, "https") || !strings.EqualFold(u.Hostname(), "opencode.ai") ||
|
||||
u.Port() != "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" {
|
||||
return false
|
||||
}
|
||||
models := e.ModelList()
|
||||
if len(models) != 1 || !strings.EqualFold(strings.TrimSpace(models[0]), "deepseek-v4-flash") {
|
||||
return false
|
||||
}
|
||||
path := strings.TrimRight(u.EscapedPath(), "/")
|
||||
switch strings.ToLower(strings.TrimSpace(e.Kind)) {
|
||||
case "responses":
|
||||
return path == "/zen/go/v1"
|
||||
case "anthropic":
|
||||
return path == "/zen/go"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// HasServerWebSearchCapability reports backend-verified endpoint/model pairs
|
||||
// that the Desktop can safely expose as a provider-side web-search toggle.
|
||||
func HasServerWebSearchCapability(e *ProviderEntry) bool {
|
||||
return IsOfficialDeepSeekWebSearchEndpoint(e) || IsOpenCodeGoDeepSeekWebSearchEndpoint(e)
|
||||
}
|
||||
|
||||
// EffectiveWebSearch resolves the persisted tri-state. Official DeepSeek
|
||||
// Anthropic and Responses endpoints default on when old configuration omitted
|
||||
// web_search, while compatible third-party endpoints remain opt-in. An explicit
|
||||
|
||||
@@ -82,6 +82,53 @@ func TestEffectiveWebSearchDefaultsOnlySupportedOfficialDeepSeekAPIs(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenCodeGoDeepSeekWebSearchCapabilityIsModelScoped(t *testing.T) {
|
||||
explicitTrue := true
|
||||
tests := []struct {
|
||||
name string
|
||||
entry ProviderEntry
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "responses flash",
|
||||
entry: ProviderEntry{Kind: "responses", BaseURL: "https://opencode.ai/zen/go/v1", Models: []string{"deepseek-v4-flash"}, WebSearch: &explicitTrue},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "anthropic flash trailing slash",
|
||||
entry: ProviderEntry{Kind: "anthropic", BaseURL: "https://opencode.ai/zen/go/", Model: "deepseek-v4-flash", WebSearch: &explicitTrue},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "mixed anthropic catalog remains unverified",
|
||||
entry: ProviderEntry{Kind: "anthropic", BaseURL: "https://opencode.ai/zen/go", Models: []string{"qwen3.7-plus", "deepseek-v4-flash"}, WebSearch: &explicitTrue},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "pro remains unsupported",
|
||||
entry: ProviderEntry{Kind: "responses", BaseURL: "https://opencode.ai/zen/go/v1", Model: "deepseek-v4-pro", WebSearch: &explicitTrue},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "wrong responses path",
|
||||
entry: ProviderEntry{Kind: "responses", BaseURL: "https://opencode.ai/zen/go", Model: "deepseek-v4-flash", WebSearch: &explicitTrue},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "lookalike host",
|
||||
entry: ProviderEntry{Kind: "anthropic", BaseURL: "https://opencode.ai.attacker.example/zen/go", Model: "deepseek-v4-flash", WebSearch: &explicitTrue},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := HasServerWebSearchCapability(&tt.entry); got != tt.want {
|
||||
t.Fatalf("HasServerWebSearchCapability(%+v) = %t, want %t", tt.entry, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebSearchTOMLRoundTripPreservesExplicitOff(t *testing.T) {
|
||||
explicitFalse := false
|
||||
cfg := &Config{Providers: []ProviderEntry{{
|
||||
|
||||
@@ -13,6 +13,77 @@ import (
|
||||
"reasonix/internal/provider"
|
||||
)
|
||||
|
||||
// TestRealOpenCodeGoDeepSeekAnthropicWebSearch exercises Reasonix's complete
|
||||
// Messages serialization and server-side web search parser against the OpenCode
|
||||
// Go DeepSeek Flash route. The key stays process-local; ordinary CI never runs it.
|
||||
func TestRealOpenCodeGoDeepSeekAnthropicWebSearch(t *testing.T) {
|
||||
key := os.Getenv("OPENCODE_GO_API_KEY")
|
||||
if key == "" {
|
||||
t.Skip("OPENCODE_GO_API_KEY not set — skipping live probe")
|
||||
}
|
||||
|
||||
p, err := New(provider.Config{
|
||||
Name: "opencode-go-deepseek-anthropic",
|
||||
BaseURL: "https://opencode.ai/zen/go",
|
||||
Model: "deepseek-v4-flash",
|
||||
APIKey: key,
|
||||
Extra: map[string]any{
|
||||
"api_key_env": "OPENCODE_GO_API_KEY",
|
||||
"thinking": "adaptive",
|
||||
"effort": "high",
|
||||
"web_search": true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
|
||||
turn := collectLiveDeepSeekTurn(t, p, provider.Request{Messages: []provider.Message{{
|
||||
Role: provider.RoleUser, Content: "Search the web for the OpenCode Go documentation and reply with one source URL.",
|
||||
}}, MaxTokens: 768})
|
||||
if strings.TrimSpace(turn.text) == "" {
|
||||
t.Fatalf("OpenCode Go Anthropic web_search returned no assistant text; reasoning_len=%d", len(turn.reasoning))
|
||||
}
|
||||
t.Logf("opencode-go-deepseek-anthropic web_search: text=%d reasoning=%d prompt=%d", len(turn.text), len(turn.reasoning), turn.promptTokens)
|
||||
}
|
||||
|
||||
// TestRealOpenCodeGoDeepSeekAnthropicToolLoop verifies that the gateway accepts
|
||||
// an assistant tool call and the corresponding tool result on the next request.
|
||||
func TestRealOpenCodeGoDeepSeekAnthropicToolLoop(t *testing.T) {
|
||||
key := os.Getenv("OPENCODE_GO_API_KEY")
|
||||
if key == "" {
|
||||
t.Skip("OPENCODE_GO_API_KEY not set — skipping live probe")
|
||||
}
|
||||
p, err := New(provider.Config{
|
||||
Name: "opencode-go-deepseek-anthropic", BaseURL: "https://opencode.ai/zen/go", Model: "deepseek-v4-flash", APIKey: key,
|
||||
Extra: map[string]any{"api_key_env": "OPENCODE_GO_API_KEY", "thinking": "adaptive", "effort": "high"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
tools := []provider.ToolSchema{{
|
||||
Name: "get_marker", Description: "Return a fixed integration-test marker. Always call this tool when the user asks for the marker.",
|
||||
Parameters: json.RawMessage(`{"type":"object","properties":{},"additionalProperties":false}`),
|
||||
}}
|
||||
messages := []provider.Message{
|
||||
{Role: provider.RoleSystem, Content: "You are a concise tool-using assistant. Call the requested tool before answering."},
|
||||
{Role: provider.RoleUser, Content: "Call get_marker, then report its result."},
|
||||
}
|
||||
first := collectLiveDeepSeekTurn(t, p, provider.Request{Messages: messages, Tools: tools, MaxTokens: 512})
|
||||
if len(first.calls) == 0 {
|
||||
t.Fatalf("OpenCode Go Anthropic returned no tool call; text_len=%d reasoning_len=%d", len(first.text), len(first.reasoning))
|
||||
}
|
||||
messages = append(messages,
|
||||
provider.Message{Role: provider.RoleAssistant, Content: first.text, ReasoningContent: first.reasoning, ReasoningSignature: first.signature, ToolCalls: first.calls},
|
||||
provider.Message{Role: provider.RoleTool, ToolCallID: first.calls[0].ID, Name: first.calls[0].Name, Content: "protocol-round-trip-ok"},
|
||||
)
|
||||
second := collectLiveDeepSeekTurn(t, p, provider.Request{Messages: messages, Tools: tools, MaxTokens: 512})
|
||||
if strings.TrimSpace(second.text) == "" {
|
||||
t.Fatalf("OpenCode Go Anthropic tool follow-up returned no text; reasoning_len=%d calls=%d", len(second.reasoning), len(second.calls))
|
||||
}
|
||||
t.Logf("opencode-go-deepseek-anthropic tool loop: calls=%d reasoning=%d signature=%d second_text=%d", len(first.calls), len(first.reasoning), len(first.signature), len(second.text))
|
||||
}
|
||||
|
||||
// TestRealDeepSeekAnthropicToolLoop exercises the official Messages endpoint's
|
||||
// unsigned thinking replay contract. It is build-tagged and credential-gated so
|
||||
// ordinary CI remains deterministic and free of live API cost.
|
||||
@@ -57,10 +128,11 @@ func TestRealDeepSeekAnthropicToolLoop(t *testing.T) {
|
||||
|
||||
messages = append(messages,
|
||||
provider.Message{
|
||||
Role: provider.RoleAssistant,
|
||||
Content: first.text,
|
||||
ReasoningContent: first.reasoning,
|
||||
ToolCalls: first.calls,
|
||||
Role: provider.RoleAssistant,
|
||||
Content: first.text,
|
||||
ReasoningContent: first.reasoning,
|
||||
ReasoningSignature: first.signature,
|
||||
ToolCalls: first.calls,
|
||||
},
|
||||
provider.Message{
|
||||
Role: provider.RoleTool,
|
||||
@@ -152,7 +224,7 @@ func TestRealDeepSeekAnthropicIgnoresImages(t *testing.T) {
|
||||
}
|
||||
|
||||
type liveDeepSeekTurn struct {
|
||||
text, reasoning string
|
||||
text, reasoning, signature string
|
||||
calls []provider.ToolCall
|
||||
promptTokens, cacheHitTokens int
|
||||
}
|
||||
@@ -173,6 +245,9 @@ func collectLiveDeepSeekTurn(t *testing.T, p provider.Provider, req provider.Req
|
||||
text.WriteString(chunk.Text)
|
||||
case provider.ChunkReasoning:
|
||||
reasoning.WriteString(chunk.Text)
|
||||
if chunk.Signature != "" {
|
||||
out.signature = chunk.Signature
|
||||
}
|
||||
case provider.ChunkToolCall:
|
||||
if chunk.ToolCall != nil {
|
||||
out.calls = append(out.calls, *chunk.ToolCall)
|
||||
|
||||
@@ -3,13 +3,86 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/provider"
|
||||
)
|
||||
|
||||
// TestRealOpenCodeGoDeepSeekResponsesWebSearch exercises Reasonix's stateless
|
||||
// Responses request, server-side web search, and replay-item capture against the
|
||||
// OpenCode Go DeepSeek Flash route.
|
||||
func TestRealOpenCodeGoDeepSeekResponsesWebSearch(t *testing.T) {
|
||||
key := os.Getenv("OPENCODE_GO_API_KEY")
|
||||
if key == "" {
|
||||
t.Skip("OPENCODE_GO_API_KEY not set — skipping live probe")
|
||||
}
|
||||
|
||||
p := New(Config{
|
||||
Name: "opencode-go-deepseek-responses",
|
||||
BaseURL: "https://opencode.ai/zen/go/v1",
|
||||
Model: "deepseek-v4-flash",
|
||||
APIKey: key,
|
||||
KeyEnv: "OPENCODE_GO_API_KEY",
|
||||
Effort: "high",
|
||||
Mode: "stateless",
|
||||
WebSearch: true,
|
||||
MaxOutputTokens: 768,
|
||||
})
|
||||
messages := []provider.Message{{
|
||||
Role: provider.RoleUser, Content: "Search the web for the OpenCode Go documentation and reply with one source URL.",
|
||||
}}
|
||||
first := collectLiveOpenCodeGoResponsesTurn(t, p, provider.Request{Messages: messages, MaxTokens: 768})
|
||||
if strings.TrimSpace(first.text) == "" {
|
||||
t.Fatal("OpenCode Go Responses web_search returned no assistant text")
|
||||
}
|
||||
if len(first.items) == 0 {
|
||||
t.Fatal("OpenCode Go Responses returned no completed web_search_call replay item")
|
||||
}
|
||||
messages = append(messages,
|
||||
provider.Message{Role: provider.RoleAssistant, Content: first.text, ResponsesItems: first.items},
|
||||
provider.Message{Role: provider.RoleUser, Content: "Using the previous search result, reply with only the source domain."},
|
||||
)
|
||||
second := collectLiveOpenCodeGoResponsesTurn(t, p, provider.Request{Messages: messages, MaxTokens: 256})
|
||||
if strings.TrimSpace(second.text) == "" {
|
||||
t.Fatal("OpenCode Go Responses stateless replay returned no follow-up text")
|
||||
}
|
||||
t.Logf("opencode-go-deepseek-responses: first_text=%d replay_items=%d second_text=%d", len(first.text), len(first.items), len(second.text))
|
||||
}
|
||||
|
||||
type liveOpenCodeGoResponsesTurn struct {
|
||||
text string
|
||||
items []json.RawMessage
|
||||
}
|
||||
|
||||
func collectLiveOpenCodeGoResponsesTurn(t *testing.T, p provider.Provider, req provider.Request) liveOpenCodeGoResponsesTurn {
|
||||
t.Helper()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
stream, err := p.Stream(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("Stream: %v", err)
|
||||
}
|
||||
var out liveOpenCodeGoResponsesTurn
|
||||
var text strings.Builder
|
||||
for chunk := range stream {
|
||||
switch chunk.Type {
|
||||
case provider.ChunkText:
|
||||
text.WriteString(chunk.Text)
|
||||
case provider.ChunkResponsesItem:
|
||||
out.items = append(out.items, append(json.RawMessage(nil), chunk.ResponsesItem...))
|
||||
case provider.ChunkError:
|
||||
t.Fatalf("stream error: %v", chunk.Err)
|
||||
}
|
||||
}
|
||||
out.text = text.String()
|
||||
return out
|
||||
}
|
||||
|
||||
// TestRealDeepSeekResponsesWebSearch exercises the official stateless
|
||||
// Responses endpoint with its provider-executed web_search tool. It is
|
||||
// credential-gated and build-tagged so ordinary CI remains deterministic.
|
||||
|
||||
@@ -348,7 +348,7 @@ func (c *client) buildRequestBody(req provider.Request) (map[string]any, bool, [
|
||||
return body, true, messages
|
||||
}
|
||||
|
||||
body["input"] = messagesToInput(rest, c.vision, c.vendor == "deepseek", c.caps.summaryRequired)
|
||||
body["input"] = messagesToInput(rest, c.vision, c.webSearch, c.caps.summaryRequired)
|
||||
return body, false, messages
|
||||
}
|
||||
|
||||
@@ -359,7 +359,7 @@ func splitInstructions(messages []provider.Message) (string, []provider.Message)
|
||||
return messages[0].Content, messages[1:]
|
||||
}
|
||||
|
||||
func messagesToInput(messages []provider.Message, vision, replayDeepSeekItems, summary bool) []map[string]any {
|
||||
func messagesToInput(messages []provider.Message, vision, replayWebSearchItems, summary bool) []map[string]any {
|
||||
input := make([]map[string]any, 0, len(messages)*2)
|
||||
for _, message := range messages {
|
||||
switch message.Role {
|
||||
@@ -409,7 +409,7 @@ func messagesToInput(messages []provider.Message, vision, replayDeepSeekItems, s
|
||||
}
|
||||
input = append(input, item)
|
||||
}
|
||||
if replayDeepSeekItems {
|
||||
if replayWebSearchItems {
|
||||
for _, raw := range message.ResponsesItems {
|
||||
if item, ok := decodeReplayableWebSearchItem(raw); ok {
|
||||
input = append(input, item)
|
||||
@@ -459,7 +459,7 @@ func (c *client) conversationDigest(messages []provider.Message) string {
|
||||
payload, _ := json.Marshal(struct {
|
||||
Instructions string `json:"instructions,omitempty"`
|
||||
Input []map[string]any `json:"input"`
|
||||
}{Instructions: instructions, Input: messagesToInput(rest, c.vision, c.vendor == "deepseek", c.caps.summaryRequired)})
|
||||
}{Instructions: instructions, Input: messagesToInput(rest, c.vision, c.webSearch, c.caps.summaryRequired)})
|
||||
sum := sha256.Sum256(payload)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
@@ -618,7 +618,7 @@ func (c *client) readStream(ctx context.Context, resp *http.Response, out chan<-
|
||||
}
|
||||
}
|
||||
case "response.output_item.done":
|
||||
if event.Item != nil && event.Item.Type == "web_search_call" && c.vendor == "deepseek" {
|
||||
if event.Item != nil && event.Item.Type == "web_search_call" && c.webSearch {
|
||||
if _, ok := decodeReplayableWebSearchItem(event.Item.Raw); ok {
|
||||
key := event.Item.ID
|
||||
if key == "" {
|
||||
|
||||
@@ -302,7 +302,7 @@ func TestStreamToleratesWebSearchLifecycleEvents(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeepSeekStatelessReplayPreservesCompletedWebSearchCall(t *testing.T) {
|
||||
func TestEnabledStatelessWebSearchPreservesCompletedCallForCompatibleGateway(t *testing.T) {
|
||||
var bodies []map[string]any
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var body map[string]any
|
||||
@@ -323,10 +323,7 @@ func TestDeepSeekStatelessReplayPreservesCompletedWebSearchCall(t *testing.T) {
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := New(Config{Name: "deepseek", APIKey: "key", BaseURL: server.URL, Model: "deepseek-v4-flash", Mode: "stateless", WebSearch: true}).(*client)
|
||||
// The test server is local, so pin the vendor classification to the official
|
||||
// DeepSeek behavior under test without weakening production URL detection.
|
||||
client.vendor = "deepseek"
|
||||
client := New(Config{Name: "compatible", APIKey: "key", BaseURL: server.URL, Model: "deepseek-v4-flash", Mode: "stateless", WebSearch: true}).(*client)
|
||||
first := collect(t, client, provider.Request{Messages: []provider.Message{{Role: provider.RoleUser, Content: "search"}}})
|
||||
var replayItems []json.RawMessage
|
||||
for _, chunk := range first {
|
||||
@@ -360,7 +357,7 @@ func TestDeepSeekStatelessReplayPreservesCompletedWebSearchCall(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResponsesItemsAreIgnoredOutsideOfficialDeepSeekWire(t *testing.T) {
|
||||
func TestResponsesItemsAreIgnoredWhenServerWebSearchIsDisabled(t *testing.T) {
|
||||
raw := json.RawMessage(`{"id":"ws_1","type":"web_search_call","status":"completed"}`)
|
||||
client := New(Config{Name: "compatible", BaseURL: "https://gateway.example", Model: "m", Mode: "stateless"}).(*client)
|
||||
body, _, _ := client.buildRequestBody(provider.Request{Messages: []provider.Message{
|
||||
@@ -383,7 +380,7 @@ func TestDeepSeekReplayDropsMalformedOrIncompleteSearchItems(t *testing.T) {
|
||||
json.RawMessage(`{"id":"fc_1","type":"function_call","status":"completed"}`),
|
||||
json.RawMessage(`{"id":`),
|
||||
}
|
||||
client := New(Config{Name: "deepseek", BaseURL: "https://api.deepseek.com", Model: "deepseek-v4-flash", Mode: "stateless"}).(*client)
|
||||
client := New(Config{Name: "deepseek", BaseURL: "https://api.deepseek.com", Model: "deepseek-v4-flash", Mode: "stateless", WebSearch: true}).(*client)
|
||||
body, _, _ := client.buildRequestBody(provider.Request{Messages: []provider.Message{
|
||||
{Role: provider.RoleUser, Content: "search"},
|
||||
{Role: provider.RoleAssistant, Content: "answer", ResponsesItems: items},
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package responses
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/provider"
|
||||
)
|
||||
|
||||
func TestConversationDigestIncludesEnabledWebSearchReplayItems(t *testing.T) {
|
||||
messages := []provider.Message{
|
||||
{Role: provider.RoleUser, Content: "search"},
|
||||
{Role: provider.RoleAssistant, Content: "found", ResponsesItems: []json.RawMessage{
|
||||
json.RawMessage(`{"id":"ws_1","type":"web_search_call","status":"completed"}`),
|
||||
}},
|
||||
}
|
||||
searchOff := New(Config{Name: "compatible", BaseURL: "https://gateway.example", Model: "m", Mode: "stateful"}).(*client)
|
||||
searchOn := New(Config{Name: "compatible", BaseURL: "https://gateway.example", Model: "m", Mode: "stateful", WebSearch: true}).(*client)
|
||||
if searchOff.conversationDigest(messages) == searchOn.conversationDigest(messages) {
|
||||
t.Fatal("web search replay items must change the digest (wire sends completed search calls)")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user