From fbb8545890ca32725b1afaeddd0a0d1be177396d Mon Sep 17 00:00:00 2001 From: svemagie <869694+svemagie@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:05:52 +0100 Subject: [PATCH] Fix CORS: replace fetch with requestUrl for all API calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Native fetch from app://obsidian.md is blocked by Anthropic's CORS policy regardless of headers. requestUrl (Obsidian's native HTTP client) bypasses CORS entirely. Real streaming is dropped — response arrives as one chunk. ChatView's streaming loop continues to work unchanged. Co-Authored-By: Claude Sonnet 4.6 --- main.js | 61 +++++++++++-------------------------------- src/ClaudeClient.ts | 63 ++++++++++----------------------------------- 2 files changed, 29 insertions(+), 95 deletions(-) diff --git a/main.js b/main.js index bc3a65e..07317f3 100644 --- a/main.js +++ b/main.js @@ -657,65 +657,34 @@ var ClaudeClient = class { "anthropic-version": "2023-06-01" }; } - /** Stream a chat completion, yielding text chunks */ + /** + * "Stream" a chat completion via requestUrl (no real streaming — CORS blocks + * native fetch from app://obsidian.md). Yields the full response as a single + * text chunk so ChatView's streaming loop keeps working unchanged. + */ async *streamChat(messages, options) { - var _a, _b, _c, _d, _e, _f; - const response = await fetch(this.baseUrl, { + var _a, _b, _c, _d; + const response = await (0, import_obsidian2.requestUrl)({ + url: this.baseUrl, method: "POST", headers: this.headers(options.apiKey), body: JSON.stringify({ model: options.model, max_tokens: (_a = options.maxTokens) != null ? _a : 2048, - stream: true, system: options.systemPrompt, messages - }) + }), + throw: false }); - if (!response.ok) { - const err = await response.text(); - yield { type: "error", error: `API Error ${response.status}: ${err}` }; + if (response.status >= 400) { + yield { type: "error", error: `API Error ${response.status}: ${response.text}` }; return; } - const reader = (_b = response.body) == null ? void 0 : _b.getReader(); - if (!reader) { - yield { type: "error", error: "No response body" }; - return; - } - const decoder = new TextDecoder(); - let buffer = ""; - while (true) { - const { done, value } = await reader.read(); - if (done) - break; - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = (_c = lines.pop()) != null ? _c : ""; - for (const line of lines) { - if (!line.startsWith("data: ")) - continue; - const data = line.slice(6).trim(); - if (data === "[DONE]") { - yield { type: "done" }; - return; - } - try { - const json = JSON.parse(data); - if (json.type === "content_block_delta" && ((_d = json.delta) == null ? void 0 : _d.type) === "text_delta") { - yield { type: "text", text: json.delta.text }; - } else if (json.type === "message_stop") { - yield { type: "done" }; - return; - } else if (json.type === "error") { - yield { type: "error", error: (_f = (_e = json.error) == null ? void 0 : _e.message) != null ? _f : "Unknown error" }; - return; - } - } catch (e) { - } - } - } + const text = (_d = (_c = (_b = response.json.content) == null ? void 0 : _b[0]) == null ? void 0 : _c.text) != null ? _d : ""; + yield { type: "text", text }; yield { type: "done" }; } - /** Non-streaming version — uses Obsidian's requestUrl to bypass CORS */ + /** Non-streaming convenience wrapper */ async chat(messages, options) { var _a, _b, _c, _d; const response = await (0, import_obsidian2.requestUrl)({ diff --git a/src/ClaudeClient.ts b/src/ClaudeClient.ts index 2474e26..0c857e9 100644 --- a/src/ClaudeClient.ts +++ b/src/ClaudeClient.ts @@ -18,7 +18,7 @@ export interface ClaudeStreamChunk { error?: string; } -/** Minimal Claude API client */ +/** Minimal Claude API client using Obsidian's requestUrl (bypasses CORS) */ export class ClaudeClient { private baseUrl = "https://api.anthropic.com/v1/messages"; @@ -30,74 +30,39 @@ export class ClaudeClient { }; } - /** Stream a chat completion, yielding text chunks */ + /** + * "Stream" a chat completion via requestUrl (no real streaming — CORS blocks + * native fetch from app://obsidian.md). Yields the full response as a single + * text chunk so ChatView's streaming loop keeps working unchanged. + */ async *streamChat( messages: ClaudeMessage[], options: ClaudeOptions ): AsyncGenerator { - // Use native fetch for streaming (requestUrl doesn't support streaming). - // The outdated anthropic-beta header is omitted — streaming is stable API. - const response = await fetch(this.baseUrl, { + const response = await requestUrl({ + url: this.baseUrl, method: "POST", headers: this.headers(options.apiKey), body: JSON.stringify({ model: options.model, max_tokens: options.maxTokens ?? 2048, - stream: true, system: options.systemPrompt, messages, }), + throw: false, }); - if (!response.ok) { - const err = await response.text(); - yield { type: "error", error: `API Error ${response.status}: ${err}` }; + if (response.status >= 400) { + yield { type: "error", error: `API Error ${response.status}: ${response.text}` }; return; } - const reader = response.body?.getReader(); - if (!reader) { - yield { type: "error", error: "No response body" }; - return; - } - - const decoder = new TextDecoder(); - let buffer = ""; - - 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() ?? ""; - - for (const line of lines) { - if (!line.startsWith("data: ")) continue; - const data = line.slice(6).trim(); - if (data === "[DONE]") { - yield { type: "done" }; - return; - } - try { - const json = JSON.parse(data); - if (json.type === "content_block_delta" && json.delta?.type === "text_delta") { - yield { type: "text", text: json.delta.text }; - } else if (json.type === "message_stop") { - yield { type: "done" }; - return; - } else if (json.type === "error") { - yield { type: "error", error: json.error?.message ?? "Unknown error" }; - return; - } - } catch { - // skip malformed lines - } - } - } + const text: string = response.json.content?.[0]?.text ?? ""; + yield { type: "text", text }; yield { type: "done" }; } - /** Non-streaming version — uses Obsidian's requestUrl to bypass CORS */ + /** Non-streaming convenience wrapper */ async chat(messages: ClaudeMessage[], options: ClaudeOptions): Promise { const response = await requestUrl({ url: this.baseUrl,