feat: integrate Z.AI, Ollama Cloud, and OpenCode Zen free models

Added comprehensive AI model integrations:

Z.AI Integration:
- Client with Anthropic-compatible API (GLM Coding Plan)
- Routes for config, testing, and streaming chat
- Settings UI component with API key management

OpenCode Zen Integration:
- Free models client using 'public' API key
- Dynamic model fetching from models.dev
- Supports GPT-5 Nano, Big Pickle, Grok Code Fast 1, MiniMax M2.1
- No API key required for free tier!

UI Enhancements:
- Added Free Models tab (first position) in Advanced Settings
- Z.AI tab with GLM Coding Plan info
- OpenCode Zen settings with model cards and status

All integrations work standalone without opencode.exe dependency.
This commit is contained in:
Gemini AI
2025-12-23 14:26:03 +04:00
Unverified
parent cdd31feead
commit 68b6c39934
8 changed files with 1316 additions and 17 deletions

View File

@@ -0,0 +1,241 @@
/**
* Z.AI API Integration
* Provides access to Z.AI's GLM Coding Plan API (Anthropic-compatible)
* Based on https://docs.z.ai/devpack/tool/claude#step-2-config-glm-coding-plan
*/
import { z } from "zod"
// Configuration schema for Z.AI
export const ZAIConfigSchema = z.object({
apiKey: z.string().optional(),
endpoint: z.string().default("https://api.z.ai/api/anthropic"),
enabled: z.boolean().default(false),
timeout: z.number().default(3000000) // 50 minutes as per docs
})
export type ZAIConfig = z.infer<typeof ZAIConfigSchema>
// Message schema (Anthropic-compatible)
export const ZAIMessageSchema = z.object({
role: z.enum(["user", "assistant"]),
content: z.string()
})
export type ZAIMessage = z.infer<typeof ZAIMessageSchema>
// Chat request schema
export const ZAIChatRequestSchema = z.object({
model: z.string().default("claude-sonnet-4-20250514"),
messages: z.array(ZAIMessageSchema),
max_tokens: z.number().default(8192),
stream: z.boolean().default(true),
system: z.string().optional()
})
export type ZAIChatRequest = z.infer<typeof ZAIChatRequestSchema>
// Chat response schema
export const ZAIChatResponseSchema = z.object({
id: z.string(),
type: z.string(),
role: z.string(),
content: z.array(z.object({
type: z.string(),
text: z.string().optional()
})),
model: z.string(),
stop_reason: z.string().nullable().optional(),
stop_sequence: z.string().nullable().optional(),
usage: z.object({
input_tokens: z.number(),
output_tokens: z.number()
}).optional()
})
export type ZAIChatResponse = z.infer<typeof ZAIChatResponseSchema>
// Stream chunk schema
export const ZAIStreamChunkSchema = z.object({
type: z.string(),
index: z.number().optional(),
delta: z.object({
type: z.string().optional(),
text: z.string().optional()
}).optional(),
message: z.object({
id: z.string(),
type: z.string(),
role: z.string(),
content: z.array(z.any()),
model: z.string()
}).optional(),
content_block: z.object({
type: z.string(),
text: z.string()
}).optional()
})
export type ZAIStreamChunk = z.infer<typeof ZAIStreamChunkSchema>
export class ZAIClient {
private config: ZAIConfig
private baseUrl: string
constructor(config: ZAIConfig) {
this.config = config
this.baseUrl = config.endpoint.replace(/\/$/, "") // Remove trailing slash
}
/**
* Test connection to Z.AI API
*/
async testConnection(): Promise<boolean> {
if (!this.config.apiKey) {
return false
}
try {
// Make a minimal request to test auth
const response = await fetch(`${this.baseUrl}/v1/messages`, {
method: "POST",
headers: this.getHeaders(),
body: JSON.stringify({
model: "claude-sonnet-4-20250514",
max_tokens: 1,
messages: [{ role: "user", content: "test" }]
})
})
// Any response other than auth error means connection works
return response.status !== 401 && response.status !== 403
} catch (error) {
console.error("Z.AI connection test failed:", error)
return false
}
}
/**
* List available models
*/
async listModels(): Promise<string[]> {
// Z.AI provides access to Claude models through their proxy
return [
"claude-sonnet-4-20250514",
"claude-3-5-sonnet-20241022",
"claude-3-opus-20240229",
"claude-3-haiku-20240307"
]
}
/**
* Chat completion (streaming)
*/
async *chatStream(request: ZAIChatRequest): AsyncGenerator<ZAIStreamChunk> {
if (!this.config.apiKey) {
throw new Error("Z.AI API key is required")
}
const response = await fetch(`${this.baseUrl}/v1/messages`, {
method: "POST",
headers: this.getHeaders(),
body: JSON.stringify({
...request,
stream: true
})
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Z.AI API error (${response.status}): ${errorText}`)
}
if (!response.body) {
throw new Error("Response body is missing")
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ""
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split("\n")
buffer = lines.pop() || "" // Keep incomplete line in buffer
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6).trim()
if (data === "[DONE]") return
try {
const parsed = JSON.parse(data)
yield parsed as ZAIStreamChunk
} catch (e) {
// Skip invalid JSON
}
}
}
}
} finally {
reader.releaseLock()
}
}
/**
* Chat completion (non-streaming)
*/
async chat(request: ZAIChatRequest): Promise<ZAIChatResponse> {
if (!this.config.apiKey) {
throw new Error("Z.AI API key is required")
}
const response = await fetch(`${this.baseUrl}/v1/messages`, {
method: "POST",
headers: this.getHeaders(),
body: JSON.stringify({
...request,
stream: false
})
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Z.AI API error (${response.status}): ${errorText}`)
}
return await response.json()
}
/**
* Get request headers
*/
private getHeaders(): Record<string, string> {
return {
"Content-Type": "application/json",
"x-api-key": this.config.apiKey || "",
"anthropic-version": "2023-06-01"
}
}
/**
* Validate API key
*/
static validateApiKey(apiKey: string): boolean {
return typeof apiKey === "string" && apiKey.length > 0
}
}
// Default available models
export const ZAI_MODELS = [
"claude-sonnet-4-20250514",
"claude-3-5-sonnet-20241022",
"claude-3-opus-20240229",
"claude-3-haiku-20240307"
] as const
export type ZAIModelName = typeof ZAI_MODELS[number]