Skip to content

Commit 3c60204

Browse files
authored
Merge pull request #196 from bramses/settings-improvements
adds z.ai Provider and simplifies the settings UI
2 parents d5cf48b + b18d014 commit 3c60204

20 files changed

+763
-336
lines changed

package.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,23 @@
2121
"keywords": [],
2222
"author": "",
2323
"dependencies": {
24-
"@ai-sdk/anthropic": "^3.0.35",
25-
"@ai-sdk/google": "^3.0.20",
26-
"@ai-sdk/openai": "^3.0.25",
27-
"@ai-sdk/openai-compatible": "^2.0.26",
24+
"@ai-sdk/anthropic": "^3.0.38",
25+
"@ai-sdk/google": "^3.0.22",
26+
"@ai-sdk/openai": "^3.0.26",
27+
"@ai-sdk/openai-compatible": "^2.0.28",
2828
"@openrouter/ai-sdk-provider": "^2.1.1",
29-
"ai": "6.0.67",
29+
"ai": "6.0.77",
3030
"zod": "4.3.6"
3131
},
3232
"devDependencies": {
33-
"@codemirror/state": "^6.5.4",
34-
"@codemirror/view": "^6.39.12",
35-
"@types/node": "^25.2.0",
33+
"@codemirror/state": "6.5.0",
34+
"@codemirror/view": "6.38.6",
35+
"@types/node": "^25.2.2",
3636
"@typescript-eslint/eslint-plugin": "8.54.0",
3737
"@typescript-eslint/parser": "8.54.0",
3838
"builtin-modules": "5.0.0",
39-
"esbuild": "0.27.2",
40-
"eslint": "^9.39.2",
39+
"esbuild": "0.27.3",
40+
"eslint": "^9.0.0",
4141
"obsidian": "latest",
4242
"prettier": "^3.8.1",
4343
"tslib": "2.8.1",

src/Commands/CommandUtilities.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
AI_SERVICE_OLLAMA,
1010
AI_SERVICE_OPENAI,
1111
AI_SERVICE_OPENROUTER,
12+
AI_SERVICE_ZAI,
1213
FETCH_MODELS_TIMEOUT_MS,
1314
} from "src/Constants";
1415
import { getApiUrlsFromFrontmatter } from "src/Utilities/FrontmatterHelpers";
@@ -19,6 +20,7 @@ import {
1920
DEFAULT_OLLAMA_CONFIG,
2021
DEFAULT_OPENAI_CONFIG,
2122
DEFAULT_OPENROUTER_CONFIG,
23+
DEFAULT_ZAI_CONFIG,
2224
} from "src/Services/DefaultConfigs";
2325

2426
/**
@@ -40,6 +42,7 @@ export function getDefaultApiUrls(settings: ChatGPT_MDSettings): { [key: string]
4042
[AI_SERVICE_LMSTUDIO]: settings.lmstudioUrl || DEFAULT_LMSTUDIO_CONFIG.url,
4143
[AI_SERVICE_ANTHROPIC]: settings.anthropicUrl || DEFAULT_ANTHROPIC_CONFIG.url,
4244
[AI_SERVICE_GEMINI]: settings.geminiUrl || DEFAULT_GEMINI_CONFIG.url,
45+
[AI_SERVICE_ZAI]: settings.zaiUrl || DEFAULT_ZAI_CONFIG.url,
4346
};
4447
}
4548

@@ -140,6 +143,18 @@ export async function fetchAvailableModels(
140143
);
141144
}
142145

146+
// Conditionally add Z.AI promise
147+
const zaiApiKey = apiAuthService.getApiKey(settingsService.getSettings(), AI_SERVICE_ZAI);
148+
if (isValidApiKey(zaiApiKey)) {
149+
promises.push(
150+
withTimeout(
151+
aiService.fetchAvailableModels(urls[AI_SERVICE_ZAI], zaiApiKey, settingsService.getSettings(), "zai"),
152+
FETCH_MODELS_TIMEOUT_MS,
153+
[]
154+
)
155+
);
156+
}
157+
143158
// Fetch all models in parallel and flatten the results
144159
const results = await Promise.all(promises);
145160
return results.flat();

src/Commands/ModelSelectHandler.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import {
88
AI_SERVICE_OLLAMA,
99
AI_SERVICE_OPENAI,
1010
AI_SERVICE_OPENROUTER,
11+
AI_SERVICE_ZAI,
1112
} from "src/Constants";
13+
import { DEFAULT_ZAI_CONFIG } from "src/Services/DefaultConfigs";
1214
import { fetchAvailableModels, getAiApiUrls } from "./CommandUtilities";
1315

1416
/**
@@ -61,6 +63,8 @@ export class ModelSelectHandler {
6163
[AI_SERVICE_ANTHROPIC]:
6264
frontmatter.anthropicUrl || settings.anthropicUrl || getAiApiUrls(frontmatter).anthropic,
6365
[AI_SERVICE_GEMINI]: frontmatter.geminiUrl || settings.geminiUrl || getAiApiUrls(frontmatter).gemini,
66+
[AI_SERVICE_ZAI]:
67+
frontmatter.zaiUrl || settings.zaiUrl || getAiApiUrls(frontmatter).zai || DEFAULT_ZAI_CONFIG.url,
6468
};
6569

6670
const aiService = this.services.aiProviderService();
@@ -115,6 +119,7 @@ export class ModelSelectHandler {
115119
[AI_SERVICE_LMSTUDIO]: settings.lmstudioUrl || getAiApiUrls({}).lmstudio,
116120
[AI_SERVICE_ANTHROPIC]: settings.anthropicUrl || getAiApiUrls({}).anthropic,
117121
[AI_SERVICE_GEMINI]: settings.geminiUrl || getAiApiUrls({}).gemini,
122+
[AI_SERVICE_ZAI]: settings.zaiUrl || getAiApiUrls({}).zai || DEFAULT_ZAI_CONFIG.url,
118123
};
119124

120125
const aiService = this.services.aiProviderService();

src/Constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const AI_SERVICE_OPENROUTER = "openrouter";
99
export const AI_SERVICE_LMSTUDIO = "lmstudio";
1010
export const AI_SERVICE_ANTHROPIC = "anthropic";
1111
export const AI_SERVICE_GEMINI = "gemini";
12+
export const AI_SERVICE_ZAI = "zai";
1213

1314
// API endpoints for each service
1415
export const API_ENDPOINTS = {

src/Models/Config.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
DEFAULT_OLLAMA_CONFIG,
1212
DEFAULT_OPENAI_CONFIG,
1313
DEFAULT_OPENROUTER_CONFIG,
14+
DEFAULT_ZAI_CONFIG,
1415
} from "src/Services/DefaultConfigs";
1516
import { getDefaultToolWhitelist } from "src/Services/ToolSupportDetector";
1617

@@ -49,6 +50,8 @@ export interface ApiKeySettings {
4950
anthropicApiKey: string;
5051
/** API Key for Gemini - used for Google Gemini models */
5152
geminiApiKey: string;
53+
/** API Key for Z.AI - used for GLM models (both Standard API and Coding Plan) */
54+
zaiApiKey: string;
5255
}
5356

5457
/**
@@ -154,6 +157,18 @@ export interface LmStudioFrontmatterSettings {
154157
lmstudioDefaultFrequencyPenalty: number;
155158
}
156159

160+
/**
161+
* Provider-specific frontmatter settings for Z.AI
162+
*/
163+
export interface ZaiFrontmatterSettings {
164+
/** Default model for Z.AI chats */
165+
zaiDefaultModel: string;
166+
/** Default temperature for Z.AI chats */
167+
zaiDefaultTemperature: number;
168+
/** Default max tokens for Z.AI chats */
169+
zaiDefaultMaxTokens: number;
170+
}
171+
157172
/**
158173
* Chat template settings
159174
*/
@@ -178,6 +193,8 @@ export interface ServiceUrlSettings {
178193
anthropicUrl: string;
179194
/** URL for Gemini API */
180195
geminiUrl: string;
196+
/** URL for Z.AI API (Standard: /api/paas/v4, Coding Plan: /api/anthropic) */
197+
zaiUrl: string;
181198
}
182199

183200
/**
@@ -211,7 +228,8 @@ export interface ChatGPT_MDSettings
211228
GeminiFrontmatterSettings,
212229
OpenRouterFrontmatterSettings,
213230
OllamaFrontmatterSettings,
214-
LmStudioFrontmatterSettings {}
231+
LmStudioFrontmatterSettings,
232+
ZaiFrontmatterSettings {}
215233

216234
/**
217235
* Default settings
@@ -222,6 +240,7 @@ export const DEFAULT_SETTINGS: ChatGPT_MDSettings = {
222240
openrouterApiKey: "",
223241
anthropicApiKey: "",
224242
geminiApiKey: "",
243+
zaiApiKey: "",
225244

226245
// Service URLs
227246
openaiUrl: DEFAULT_OPENAI_CONFIG.url,
@@ -230,6 +249,7 @@ export const DEFAULT_SETTINGS: ChatGPT_MDSettings = {
230249
lmstudioUrl: DEFAULT_LMSTUDIO_CONFIG.url,
231250
anthropicUrl: DEFAULT_ANTHROPIC_CONFIG.url,
232251
geminiUrl: DEFAULT_GEMINI_CONFIG.url,
252+
zaiUrl: DEFAULT_ZAI_CONFIG.url,
233253

234254
// Folders
235255
chatFolder: "ChatGPT_MD/chats",
@@ -294,4 +314,9 @@ export const DEFAULT_SETTINGS: ChatGPT_MDSettings = {
294314
lmstudioDefaultTopP: DEFAULT_LMSTUDIO_CONFIG.top_p,
295315
lmstudioDefaultPresencePenalty: DEFAULT_LMSTUDIO_CONFIG.presence_penalty,
296316
lmstudioDefaultFrequencyPenalty: DEFAULT_LMSTUDIO_CONFIG.frequency_penalty,
317+
318+
// Z.AI Defaults
319+
zaiDefaultModel: DEFAULT_ZAI_CONFIG.model,
320+
zaiDefaultTemperature: DEFAULT_ZAI_CONFIG.temperature,
321+
zaiDefaultMaxTokens: DEFAULT_ZAI_CONFIG.max_tokens,
297322
};

src/Services/Adapters/AnthropicAdapter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ export class AnthropicAdapter extends BaseProviderAdapter {
4343
}
4444

4545
try {
46-
const modelsUrl = `${url.replace(/\/$/, "")}/v1/models`;
46+
const apiPath = this.getApiPathSuffix(url);
47+
const modelsUrl = `${url.replace(/\/$/, "")}${apiPath}/models`;
4748
const headers = this.getAuthHeaders(apiKey!); // Non-null assertion: validated above
4849

4950
// Anthropic uses requestUrl directly (not through apiService)

src/Services/Adapters/BaseProviderAdapter.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,13 @@ export abstract class BaseProviderAdapter implements ProviderAdapter {
101101
}
102102
return true;
103103
}
104+
105+
/**
106+
* Default API path suffix for chat completions
107+
* Most OpenAI-compatible providers use "/v1"
108+
* @param url - Optional URL parameter (ignored by most providers)
109+
*/
110+
getApiPathSuffix(_url?: string): string {
111+
return "/v1";
112+
}
104113
}

src/Services/Adapters/CLAUDE.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Each adapter encapsulates provider-specific logic, allowing `AiProviderService`
1313
### ProviderType
1414

1515
```typescript
16-
type ProviderType = "openai" | "anthropic" | "ollama" | "openrouter" | "gemini" | "lmstudio";
16+
type ProviderType = "openai" | "anthropic" | "ollama" | "openrouter" | "gemini" | "lmstudio" | "zai";
1717
```
1818

1919
### AiProviderConfig
@@ -51,19 +51,22 @@ Contract each adapter must implement:
5151
- `supportsToolCalling()` - Tool calling support
5252
- `requiresApiKey()` - Local providers: false
5353
- `extractModelName(modelId)` - Strip provider prefix
54+
- `getApiPathSuffix()` - API path suffix for chat completions (e.g., "/v1", "/api/v1", "")
5455

5556
## BaseProviderAdapter.ts
5657

5758
**Abstract base class with common functionality**
5859

5960
Default implementations:
61+
6062
- `getSystemMessageRole()` → "system"
6163
- `supportsSystemField()` → false
6264
- `supportsToolCalling()` → true
6365
- `requiresApiKey()` → true
6466
- `extractModelName()` - Remove provider prefix
6567
- `validateApiKey()` - Check key presence
6668
- `handleFetchError()` - Consistent error logging
69+
- `getApiPathSuffix()` → "/v1" (default for most OpenAI-compatible providers)
6770

6871
## Adapter Implementations
6972

@@ -99,5 +102,20 @@ Default implementations:
99102
### OpenRouterAdapter.ts
100103

101104
- Default URL: `https://openrouter.ai`
105+
- `getApiPathSuffix()` → "/api/v1" (OpenRouter's unique structure)
102106
- Auth: `Authorization: Bearer {key}`
103107
- Prefixes models with `openrouter@`
108+
- Final endpoints: `/api/v1/chat/completions`, `/api/v1/models`
109+
110+
### ZaiAdapter.ts
111+
112+
- Default URL: `https://api.z.ai`
113+
- Uses `createOpenAICompatible` from AI SDK
114+
- `getApiPathSuffix(url)``/api/paas/v4` (Standard mode) or `/api/anthropic/v1` (Coding Plan mode)
115+
- Supports two API modes based on URL path:
116+
- Standard API: Uses `createOpenAICompatible` with `/api/paas/v4` path
117+
- Coding Plan (Anthropic-compatible): Uses `createOpenAICompatible` with `/api/anthropic/v1` path
118+
- Auth: `Authorization: Bearer {key}`
119+
- No models endpoint - returns known models directly:
120+
- GLM-4.5, GLM-4.6, GLM-4.6V, GLM-4.6V-Flash, GLM-4.6V-FlashX, GLM-4.7, GLM-4.7-Flash
121+
- Prefixes models with `zai@`

src/Services/Adapters/OpenRouterAdapter.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ export class OpenRouterAdapter extends BaseProviderAdapter {
3535
};
3636
}
3737

38+
/**
39+
* OpenRouter requires /api/v1 prefix for all endpoints
40+
* @param url - Optional URL parameter (ignored by OpenRouter)
41+
*/
42+
override getApiPathSuffix(_url?: string): string {
43+
return "/api/v1";
44+
}
45+
3846
async fetchModels(
3947
url: string,
4048
apiKey: string | undefined,
@@ -47,7 +55,8 @@ export class OpenRouterAdapter extends BaseProviderAdapter {
4755

4856
try {
4957
const headers = this.getAuthHeaders(apiKey!); // Non-null assertion: validated above
50-
const models = await makeGetRequest(`${url}/api/v1/models`, headers, this.type);
58+
const apiPath = this.getApiPathSuffix(url);
59+
const models = await makeGetRequest(`${url}${apiPath}/models`, headers, this.type);
5160

5261
return models.data
5362
.sort((a: OpenRouterModel, b: OpenRouterModel) => {

src/Services/Adapters/ProviderAdapter.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ChatGPT_MDSettings } from "src/Models/Config";
33
/**
44
* Union type for all supported AI providers
55
*/
6-
export type ProviderType = "openai" | "anthropic" | "ollama" | "openrouter" | "gemini" | "lmstudio";
6+
export type ProviderType = "openai" | "anthropic" | "ollama" | "openrouter" | "gemini" | "lmstudio" | "zai";
77

88
/**
99
* Unified configuration interface for all AI providers
@@ -107,4 +107,13 @@ export interface ProviderAdapter {
107107
* @param modelId - Model ID with or without provider prefix
108108
*/
109109
extractModelName(modelId: string): string;
110+
111+
/**
112+
* Get the API path suffix for chat completions endpoint
113+
* Most OpenAI-compatible providers use "/v1"
114+
* OpenRouter uses "/api/v1" to handle its unique structure
115+
* Z.AI returns "/api/paas/v4" or "/api/anthropic/v1" depending on the URL
116+
* @param url - Optional URL to determine the correct suffix (used by Z.AI for mode detection)
117+
*/
118+
getApiPathSuffix(url?: string): string;
110119
}

0 commit comments

Comments
 (0)