chore: update compiled output, docs, and CLAUDE.md model defaults
This commit is contained in:
@@ -74,7 +74,7 @@ Entry: `src/main.ts` → bundled to `main.js` via esbuild (CJS, ES2018 target).
|
||||
| Field | Default | Description |
|
||||
|---|---|---|
|
||||
| `apiKey` | `""` | Anthropic API key |
|
||||
| `model` | `claude-opus-4-5-20251101` | Claude model ID |
|
||||
| `model` | `claude-opus-4-6` | Claude model ID |
|
||||
| `maxTokens` | `8192` | Max output tokens (1024–16000) |
|
||||
| `maxContextNotes` | `6` | TF-IDF/embedding context notes per query |
|
||||
| `maxCharsPerNote` | `2500` | Characters per context note |
|
||||
@@ -118,4 +118,4 @@ Copy `main.js`, `manifest.json`, `styles.css` into `.obsidian/plugins/memex-chat
|
||||
|
||||
## Models (SettingsTab.ts)
|
||||
|
||||
Default: `claude-opus-4-5-20251101`. Update `MODELS` array and `DEFAULT_SETTINGS.model` when adding new model IDs.
|
||||
Default: `claude-opus-4-6`. Update `MODELS` array and `DEFAULT_SETTINGS.model` when adding new model IDs.
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
# Fetch Models Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add an "Aktualisieren" button next to the model dropdown in settings that fetches the 3 newest Claude models from the Anthropic API and repopulates the dropdown.
|
||||
|
||||
**Architecture:** `ClaudeClient` gains a `fetchModels()` method (reusing existing `requestUrl`/`headers` patterns). `SettingsTab` captures the `DropdownComponent` and `ButtonComponent` references, wires the button to call `fetchModels`, and rebuilds the dropdown on success.
|
||||
|
||||
**Tech Stack:** TypeScript, Obsidian plugin API (`requestUrl`, `Setting`, `DropdownComponent`, `ButtonComponent`, `Notice`), Anthropic Models API (`GET /v1/models`)
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/ClaudeClient.ts` | Add `fetchModels(apiKey)` method |
|
||||
| `src/SettingsTab.ts` | Update imports; refactor "Modell" `Setting` to capture dropdown + add button |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add `fetchModels` to `ClaudeClient`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/ClaudeClient.ts`
|
||||
|
||||
- [ ] **Step 1: Add the method after the `chat()` method**
|
||||
|
||||
In `src/ClaudeClient.ts`, add after line 86 (after `chat()`'s closing brace):
|
||||
|
||||
```typescript
|
||||
/** Fetch the 3 newest Claude models from the Anthropic Models API. */
|
||||
async fetchModels(apiKey: string): Promise<{ id: string; name: string }[]> {
|
||||
const response = await requestUrl({
|
||||
url: "https://api.anthropic.com/v1/models",
|
||||
method: "GET",
|
||||
headers: this.headers(apiKey),
|
||||
throw: false,
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`API Error ${response.status}: ${response.text}`);
|
||||
}
|
||||
|
||||
const data: { id: string; created: number }[] = response.json.data ?? [];
|
||||
if (data.length === 0) {
|
||||
throw new Error("No models returned");
|
||||
}
|
||||
|
||||
return data
|
||||
.sort((a, b) => b.created - a.created)
|
||||
.slice(0, 3)
|
||||
.map((m) => ({ id: m.id, name: m.id }));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify build passes**
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Expected: no TypeScript errors, `main.js` written successfully.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add src/ClaudeClient.ts
|
||||
git commit -m "feat: add fetchModels to ClaudeClient"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Update SettingsTab — imports and model setting
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/SettingsTab.ts:1` (import line)
|
||||
- Modify: `src/SettingsTab.ts:151-160` (the "Modell" Setting block)
|
||||
|
||||
- [ ] **Step 1: Extend the import from `"obsidian"`**
|
||||
|
||||
Replace the current import line 1:
|
||||
|
||||
```typescript
|
||||
import { App, PluginSettingTab, Setting } from "obsidian";
|
||||
```
|
||||
|
||||
With:
|
||||
|
||||
```typescript
|
||||
import { App, ButtonComponent, DropdownComponent, Notice, PluginSettingTab, Setting } from "obsidian";
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace the "Modell" Setting block**
|
||||
|
||||
Replace lines 151–160:
|
||||
|
||||
```typescript
|
||||
new Setting(containerEl)
|
||||
.setName("Modell")
|
||||
.setDesc("Welches Claude-Modell verwenden?")
|
||||
.addDropdown((drop) => {
|
||||
for (const m of MODELS) drop.addOption(m.id, m.name);
|
||||
drop.setValue(this.plugin.settings.model).onChange(async (value) => {
|
||||
this.plugin.settings.model = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
With:
|
||||
|
||||
```typescript
|
||||
let modelDrop: DropdownComponent;
|
||||
let refreshBtn: ButtonComponent;
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Modell")
|
||||
.setDesc("Welches Claude-Modell verwenden?")
|
||||
.addDropdown((drop) => {
|
||||
modelDrop = drop;
|
||||
for (const m of MODELS) drop.addOption(m.id, m.name);
|
||||
drop.setValue(this.plugin.settings.model).onChange(async (value) => {
|
||||
this.plugin.settings.model = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
})
|
||||
.addButton((btn) => {
|
||||
refreshBtn = btn;
|
||||
btn.setButtonText("Aktualisieren").onClick(async () => {
|
||||
const prev = modelDrop.getValue();
|
||||
refreshBtn.setDisabled(true);
|
||||
refreshBtn.setButtonText("...");
|
||||
try {
|
||||
const models = await this.plugin.claude.fetchModels(this.plugin.settings.apiKey);
|
||||
modelDrop.selectEl.empty();
|
||||
for (const m of models) modelDrop.addOption(m.id, m.name);
|
||||
modelDrop.setValue(prev);
|
||||
this.plugin.settings.model = modelDrop.getValue();
|
||||
await this.plugin.saveSettings();
|
||||
} catch (err) {
|
||||
new Notice("Modelle konnten nicht geladen werden: " + (err as Error).message);
|
||||
} finally {
|
||||
refreshBtn.setDisabled(false);
|
||||
refreshBtn.setButtonText("Aktualisieren");
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify build passes**
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Expected: no TypeScript errors, `main.js` written successfully.
|
||||
|
||||
- [ ] **Step 4: Manual smoke test in Obsidian**
|
||||
|
||||
1. Copy `main.js`, `manifest.json`, `styles.css` to `.obsidian/plugins/memex-chat/` in your vault
|
||||
2. Reload the plugin (or restart Obsidian)
|
||||
3. Open Settings → Memex Chat
|
||||
4. Confirm the "Modell" row has a dropdown and an "Aktualisieren" button
|
||||
5. With a valid API key set, click "Aktualisieren" — button should show "...", then restore; dropdown should show 3 model IDs
|
||||
6. With no API key, click "Aktualisieren" — a Notice should appear with an error message; dropdown should be unchanged
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/SettingsTab.ts
|
||||
git commit -m "feat: add Aktualisieren button to fetch models from API"
|
||||
```
|
||||
@@ -0,0 +1,89 @@
|
||||
# Fetch Models Design
|
||||
|
||||
**Date:** 2026-03-27
|
||||
**Status:** Approved
|
||||
|
||||
## Summary
|
||||
|
||||
Add an "Aktualisieren" button to the Model setting in the settings tab. When clicked, it fetches the 3 newest Claude models from the Anthropic Models API and updates the dropdown. Falls back to the hardcoded `MODELS` list if the request fails or returns no models.
|
||||
|
||||
## ClaudeClient changes
|
||||
|
||||
Add a new method `fetchModels(apiKey: string): Promise<{id: string, name: string}[]>` to `ClaudeClient`.
|
||||
|
||||
- URL: `"https://api.anthropic.com/v1/models"` — inline string or a separate private constant; **do not use or modify `baseUrl`** (which points to `/v1/messages`)
|
||||
- Call `requestUrl` with `throw: false` (same pattern as `streamChat`/`chat`) and `this.headers(apiKey)`
|
||||
- Response shape: `{ data: [{ id: string, created: number, display_name: string, ... }] }`
|
||||
- Throw on `response.status >= 400` with the response text
|
||||
- If `data` is empty, throw an error ("No models returned") — do not return an empty array
|
||||
- Sort `data` descending by `created`, take top 3
|
||||
- Return `{ id, name: id }` for each — use `id` as the display name (not `display_name`). Note: fetched entries will show raw IDs (e.g. `claude-opus-4-6`) while hardcoded `MODELS` show human-friendly names (e.g. `"Claude Opus 4.6 (Stärkste)"`). This is intentional — keeps the implementation simple and avoids relying on API-provided display strings.
|
||||
|
||||
## SettingsTab changes
|
||||
|
||||
**Import addition:** Add `Notice, ButtonComponent, DropdownComponent` to the `import { ... } from "obsidian"` line.
|
||||
|
||||
Convert the existing "Modell" `Setting` to capture both the `DropdownComponent` and `ButtonComponent` references by chaining `addDropdown()` and `addButton()` on the same `Setting` instance:
|
||||
|
||||
```typescript
|
||||
let modelDrop: DropdownComponent;
|
||||
let refreshBtn: ButtonComponent;
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Modell")
|
||||
.setDesc("Welches Claude-Modell verwenden?")
|
||||
.addDropdown((drop) => {
|
||||
modelDrop = drop;
|
||||
for (const m of MODELS) drop.addOption(m.id, m.name);
|
||||
drop.setValue(this.plugin.settings.model).onChange(async (value) => {
|
||||
this.plugin.settings.model = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
})
|
||||
.addButton((btn) => {
|
||||
refreshBtn = btn;
|
||||
btn.setButtonText("Aktualisieren").onClick(async () => { /* see click flow */ });
|
||||
});
|
||||
```
|
||||
|
||||
**Click flow:**
|
||||
1. Capture current value: `const prev = modelDrop.getValue()`
|
||||
2. `refreshBtn.setDisabled(true)` and `refreshBtn.setButtonText("...")`
|
||||
3. In a try/catch/finally:
|
||||
- **try:** Call `this.plugin.claude.fetchModels(this.plugin.settings.apiKey)`
|
||||
- On success: clear dropdown with `modelDrop.selectEl.empty()`, repopulate via `modelDrop.addOption(id, name)` for each fetched model, then set value to `prev` if it exists among the fetched ids, otherwise the first fetched id; save via `this.plugin.settings.model = modelDrop.getValue(); await this.plugin.saveSettings()`
|
||||
- **catch:** `new Notice("Modelle konnten nicht geladen werden: " + err.message)` — dropdown is **not** modified on error (hardcoded options remain)
|
||||
- **finally:** `refreshBtn.setDisabled(false)` and `refreshBtn.setButtonText("Aktualisieren")`
|
||||
|
||||
**Fallback:** The hardcoded `MODELS` array in `SettingsTab.ts` is unchanged and remains the initial population of the dropdown on every settings open.
|
||||
|
||||
## Data flow
|
||||
|
||||
```
|
||||
[Aktualisieren button click]
|
||||
→ capture prev = modelDrop.getValue()
|
||||
→ disable button, show "..."
|
||||
→ this.plugin.claude.fetchModels(apiKey) [throw: false, separate URL]
|
||||
→ throw if status >= 400 or data empty
|
||||
→ sort by created desc, take 3
|
||||
→ return [{id, name: id}]
|
||||
→ clear selectEl, repopulate, restore selection
|
||||
→ save model to settings
|
||||
→ finally: restore button
|
||||
```
|
||||
|
||||
## Error handling
|
||||
|
||||
| Scenario | Behaviour |
|
||||
|---|---|
|
||||
| No API key (401) | Notice shown; dropdown unchanged |
|
||||
| Network failure | Notice shown; dropdown unchanged |
|
||||
| Empty `data` array | Treated as error; Notice shown; dropdown unchanged |
|
||||
| Fewer than 3 models returned | Take all returned (no error) |
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Persisting fetched models across restarts
|
||||
- Auto-fetching on settings open or plugin startup
|
||||
- Configurable count of models to show
|
||||
- Updating `DEFAULT_SETTINGS.model` after a fetch
|
||||
@@ -32725,13 +32725,37 @@ var ClaudeClient = class {
|
||||
}
|
||||
return response.json.content?.[0]?.text ?? "";
|
||||
}
|
||||
/**
|
||||
* Fetch Claude models from the Anthropic Models API.
|
||||
* Returns the 2 newest versions of each family (opus, sonnet, haiku), in that order.
|
||||
*/
|
||||
async fetchModels(apiKey) {
|
||||
const response = await (0, import_obsidian2.requestUrl)({
|
||||
url: "https://api.anthropic.com/v1/models",
|
||||
method: "GET",
|
||||
headers: this.headers(apiKey),
|
||||
throw: false
|
||||
});
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`API Error ${response.status}: ${response.text}`);
|
||||
}
|
||||
const data = response.json.data ?? [];
|
||||
if (data.length === 0) {
|
||||
throw new Error("No models returned");
|
||||
}
|
||||
const sorted = data.sort((a, b) => b.created - a.created);
|
||||
const families = ["opus", "sonnet", "haiku"];
|
||||
return families.flatMap(
|
||||
(family) => sorted.filter((m) => m.id.includes(family)).slice(0, 2).map((m) => ({ id: m.id, name: m.id }))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// src/SettingsTab.ts
|
||||
var import_obsidian3 = require("obsidian");
|
||||
var DEFAULT_SETTINGS = {
|
||||
apiKey: "",
|
||||
model: "claude-opus-4-5-20251101",
|
||||
model: "claude-opus-4-6",
|
||||
maxTokens: 8192,
|
||||
maxContextNotes: 6,
|
||||
maxCharsPerNote: 2500,
|
||||
@@ -32768,8 +32792,8 @@ Wenn du Fragen beantwortest:
|
||||
]
|
||||
};
|
||||
var MODELS = [
|
||||
{ id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5 (St\xE4rkste)" },
|
||||
{ id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5 (Empfohlen)" },
|
||||
{ id: "claude-opus-4-6", name: "Claude Opus 4.6 (St\xE4rkste)" },
|
||||
{ id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (Empfohlen)" },
|
||||
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5 (Schnell)" }
|
||||
];
|
||||
var MemexChatSettingsTab = class extends import_obsidian3.PluginSettingTab {
|
||||
@@ -32827,13 +32851,37 @@ var MemexChatSettingsTab = class extends import_obsidian3.PluginSettingTab {
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
new import_obsidian3.Setting(containerEl).setName("Modell").setDesc("Welches Claude-Modell verwenden?").addDropdown((drop) => {
|
||||
let modelDrop;
|
||||
let refreshBtn;
|
||||
new import_obsidian3.Setting(containerEl).setName("Modell").setDesc("Welches Claude-Modell verwenden? (Aktualisieren zeigt Roh-IDs)").addDropdown((drop) => {
|
||||
modelDrop = drop;
|
||||
for (const m of MODELS)
|
||||
drop.addOption(m.id, m.name);
|
||||
drop.setValue(this.plugin.settings.model).onChange(async (value) => {
|
||||
this.plugin.settings.model = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
}).addButton((btn) => {
|
||||
refreshBtn = btn;
|
||||
btn.setButtonText("Aktualisieren").onClick(async () => {
|
||||
const prev = modelDrop.getValue();
|
||||
refreshBtn.setDisabled(true);
|
||||
refreshBtn.setButtonText("...");
|
||||
try {
|
||||
const models = await this.plugin.claude.fetchModels(this.plugin.settings.apiKey);
|
||||
modelDrop.selectEl.empty();
|
||||
for (const m of models)
|
||||
modelDrop.addOption(m.id, m.name);
|
||||
modelDrop.setValue(prev);
|
||||
this.plugin.settings.model = modelDrop.getValue();
|
||||
await this.plugin.saveSettings();
|
||||
} catch (err) {
|
||||
new import_obsidian3.Notice("Modelle konnten nicht geladen werden: " + err.message);
|
||||
} finally {
|
||||
refreshBtn.setDisabled(false);
|
||||
refreshBtn.setButtonText("Aktualisieren");
|
||||
}
|
||||
});
|
||||
});
|
||||
new import_obsidian3.Setting(containerEl).setName("Max. Antwort-Tokens").setDesc("Maximale L\xE4nge der Claude-Antwort. F\xFCr lange Analysen (z.B. Monthly Check) h\xF6her einstellen. (1024\u201316000)").addSlider(
|
||||
(slider) => slider.setLimits(1024, 16e3, 512).setValue(this.plugin.settings.maxTokens).setDynamicTooltip().onChange(async (value) => {
|
||||
@@ -33314,6 +33362,12 @@ var MemexChatPlugin = class extends import_obsidian5.Plugin {
|
||||
}
|
||||
});
|
||||
this.addSettingTab(new MemexChatSettingsTab(this.app, this));
|
||||
this.registerEvent(
|
||||
this.app.vault.on("modify", (file) => {
|
||||
if (this.embedSearch && file instanceof import_obsidian5.TFile && file.extension === "md")
|
||||
this.embedSearch.reembedFile(file);
|
||||
})
|
||||
);
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
if (!this.search.isIndexed()) {
|
||||
this.search.priorityProperties = this.settings.contextProperties;
|
||||
@@ -33366,24 +33420,19 @@ var MemexChatPlugin = class extends import_obsidian5.Plugin {
|
||||
this.embedSearch = new EmbedSearch(this.app, this.settings.embeddingModel);
|
||||
this.embedSearch.excludeFolders = this.settings.embedExcludeFolders ?? [];
|
||||
this.embedSearch.contextProperties = this.settings.contextProperties ?? [];
|
||||
this.registerEvent(
|
||||
this.app.vault.on("modify", (file) => {
|
||||
if (this.embedSearch && file instanceof import_obsidian5.TFile && file.extension === "md")
|
||||
this.embedSearch.reembedFile(file);
|
||||
})
|
||||
);
|
||||
const notice = new import_obsidian5.Notice("Memex: Embedding wird vorbereitet\u2026", 0);
|
||||
const modelShort = this.settings.embeddingModel.split("/").pop() ?? this.settings.embeddingModel;
|
||||
const notice = new import_obsidian5.Notice(`Memex [${modelShort}]: Embedding wird vorbereitet\u2026`, 0);
|
||||
this.embedSearch.onModelStatus = (status) => {
|
||||
notice.setMessage(`Memex: ${status}`);
|
||||
notice.setMessage(`Memex [${modelShort}]: ${status}`);
|
||||
};
|
||||
this.embedSearch.onProgress = (done, total, speed) => {
|
||||
const speedStr = speed > 0 ? ` \u2022 ${speed.toFixed(1)} N/s` : "";
|
||||
const remaining = speed > 0 && done < total ? (total - done) / speed : 0;
|
||||
const eta = remaining > 0 ? ` \u2022 ~${remaining < 60 ? Math.ceil(remaining) + "s" : Math.ceil(remaining / 60) + "min"}` : "";
|
||||
notice.setMessage(`Memex Embedding: ${done}/${total}${speedStr}${eta}`);
|
||||
notice.setMessage(`Memex [${modelShort}]: ${done}/${total}${speedStr}${eta}`);
|
||||
};
|
||||
this.waitForSyncIdle(notice).then(() => this.embedSearch?.buildIndex()).then(() => {
|
||||
notice.setMessage(`\u2713 Memex: ${this.app.vault.getMarkdownFiles().length} Notizen eingebettet`);
|
||||
notice.setMessage(`\u2713 Memex [${modelShort}]: ${this.app.vault.getMarkdownFiles().length} Notizen eingebettet`);
|
||||
setTimeout(() => notice.hide(), 4e3);
|
||||
this.notifyRelatedView();
|
||||
}).catch((e) => {
|
||||
|
||||
Reference in New Issue
Block a user