@@ -8,13 +8,20 @@ import {
88import { extractNonEmptyAssistantText , isLiveTestEnabled } from "openclaw/plugin-sdk/test-env" ;
99import { Type } from "typebox" ;
1010import { describe , expect , it } from "vitest" ;
11+ import { buildStaticOpencodeZenProviderConfig } from "./provider-catalog.js" ;
1112
13+ const OPENCODE_ZEN_MODELS_URL = "https://opencode.ai/zen/v1/models" ;
1214const OPENCODE_API_KEY =
1315 process . env . OPENCODE_API_KEY ?. trim ( ) || process . env . OPENCODE_ZEN_API_KEY ?. trim ( ) || "" ;
1416const LIVE_MODEL_ID =
1517 process . env . OPENCLAW_LIVE_OPENCODE_DEEPSEEK_MODEL ?. trim ( ) || "deepseek-v4-flash-free" ;
1618const LIVE = isLiveTestEnabled ( [ "OPENCODE_LIVE_TEST" ] ) && OPENCODE_API_KEY . length > 0 ;
1719const describeLive = LIVE ? describe : describe . skip ;
20+ const describeCatalogLive = isLiveTestEnabled ( [ "OPENCODE_LIVE_TEST" ] ) ? describe : describe . skip ;
21+
22+ type OpencodeModelsResponse = {
23+ data ?: Array < { id ?: unknown ; object ?: unknown } > ;
24+ } ;
1825
1926function resolveOpencodeDeepSeekLiveModel ( ) : Model < "openai-completions" > {
2027 return {
@@ -58,6 +65,47 @@ function hasReasoningContentReplay(message: AssistantMessage): boolean {
5865 ) ;
5966}
6067
68+ async function fetchOpencodeZenModelIds ( ) : Promise < string [ ] > {
69+ const response = await fetch ( OPENCODE_ZEN_MODELS_URL , {
70+ headers : { "accept-encoding" : "identity" } ,
71+ } ) ;
72+ expect ( response . ok ) . toBe ( true ) ;
73+ const json = ( await response . json ( ) ) as OpencodeModelsResponse ;
74+ return ( json . data ?? [ ] )
75+ . filter ( ( model ) => model . object === undefined || model . object === "model" )
76+ . map ( ( model ) => model . id )
77+ . filter ( ( id ) : id is string => typeof id === "string" && id . trim ( ) . length > 0 )
78+ . map ( ( id ) => id . trim ( ) . toLowerCase ( ) )
79+ . toSorted ( ) ;
80+ }
81+
82+ function listStaticOpencodeZenModelIds ( ) : string [ ] {
83+ return buildStaticOpencodeZenProviderConfig ( )
84+ . models . map ( ( model ) => model . id )
85+ . toSorted ( ) ;
86+ }
87+
88+ describeCatalogLive ( "opencode Zen live catalog drift" , ( ) => {
89+ it ( "keeps the provider-owned static seed aligned with the live model ids" , async ( ) => {
90+ const liveIds = await fetchOpencodeZenModelIds ( ) ;
91+ const staticIds = listStaticOpencodeZenModelIds ( ) ;
92+
93+ const staticIdSet = new Set ( staticIds ) ;
94+ const liveIdSet = new Set ( liveIds ) ;
95+ const missingStaticMetadata = liveIds . filter ( ( id ) => ! staticIdSet . has ( id ) ) ;
96+ const staleStaticRows = staticIds . filter ( ( id ) => ! liveIdSet . has ( id ) ) ;
97+
98+ expect (
99+ { missingStaticMetadata, staleStaticRows } ,
100+ [
101+ "OpenCode Zen live catalog drifted from the provider-owned static seed." ,
102+ "Add routing/baseUrl/cost/context/capability metadata for missing live ids," ,
103+ "or remove stale static rows if OpenCode retired them." ,
104+ ] . join ( " " ) ,
105+ ) . toEqual ( { missingStaticMetadata : [ ] , staleStaticRows : [ ] } ) ;
106+ } , 30_000 ) ;
107+ } ) ;
108+
61109describeLive ( "opencode plugin live" , ( ) => {
62110 it ( "accepts DeepSeek V4 tier-suffixed thinking replay after a tool call" , async ( ) => {
63111 const model = resolveOpencodeDeepSeekLiveModel ( ) ;
0 commit comments