Fix CORS: replace fetch with requestUrl for all API calls

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 <noreply@anthropic.com>
This commit is contained in:
svemagie
2026-03-04 21:05:52 +01:00
parent cbb490002c
commit fbb8545890
2 changed files with 29 additions and 95 deletions
+15 -46
View File
@@ -657,65 +657,34 @@ var ClaudeClient = class {
"anthropic-version": "2023-06-01" "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) { async *streamChat(messages, options) {
var _a, _b, _c, _d, _e, _f; var _a, _b, _c, _d;
const response = await fetch(this.baseUrl, { const response = await (0, import_obsidian2.requestUrl)({
url: this.baseUrl,
method: "POST", method: "POST",
headers: this.headers(options.apiKey), headers: this.headers(options.apiKey),
body: JSON.stringify({ body: JSON.stringify({
model: options.model, model: options.model,
max_tokens: (_a = options.maxTokens) != null ? _a : 2048, max_tokens: (_a = options.maxTokens) != null ? _a : 2048,
stream: true,
system: options.systemPrompt, system: options.systemPrompt,
messages messages
}) }),
throw: false
}); });
if (!response.ok) { if (response.status >= 400) {
const err = await response.text(); yield { type: "error", error: `API Error ${response.status}: ${response.text}` };
yield { type: "error", error: `API Error ${response.status}: ${err}` };
return; return;
} }
const reader = (_b = response.body) == null ? void 0 : _b.getReader(); const text = (_d = (_c = (_b = response.json.content) == null ? void 0 : _b[0]) == null ? void 0 : _c.text) != null ? _d : "";
if (!reader) { yield { type: "text", text };
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) {
}
}
}
yield { type: "done" }; yield { type: "done" };
} }
/** Non-streaming version — uses Obsidian's requestUrl to bypass CORS */ /** Non-streaming convenience wrapper */
async chat(messages, options) { async chat(messages, options) {
var _a, _b, _c, _d; var _a, _b, _c, _d;
const response = await (0, import_obsidian2.requestUrl)({ const response = await (0, import_obsidian2.requestUrl)({
+14 -49
View File
@@ -18,7 +18,7 @@ export interface ClaudeStreamChunk {
error?: string; error?: string;
} }
/** Minimal Claude API client */ /** Minimal Claude API client using Obsidian's requestUrl (bypasses CORS) */
export class ClaudeClient { export class ClaudeClient {
private baseUrl = "https://api.anthropic.com/v1/messages"; 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( async *streamChat(
messages: ClaudeMessage[], messages: ClaudeMessage[],
options: ClaudeOptions options: ClaudeOptions
): AsyncGenerator<ClaudeStreamChunk> { ): AsyncGenerator<ClaudeStreamChunk> {
// Use native fetch for streaming (requestUrl doesn't support streaming). const response = await requestUrl({
// The outdated anthropic-beta header is omitted — streaming is stable API. url: this.baseUrl,
const response = await fetch(this.baseUrl, {
method: "POST", method: "POST",
headers: this.headers(options.apiKey), headers: this.headers(options.apiKey),
body: JSON.stringify({ body: JSON.stringify({
model: options.model, model: options.model,
max_tokens: options.maxTokens ?? 2048, max_tokens: options.maxTokens ?? 2048,
stream: true,
system: options.systemPrompt, system: options.systemPrompt,
messages, messages,
}), }),
throw: false,
}); });
if (!response.ok) { if (response.status >= 400) {
const err = await response.text(); yield { type: "error", error: `API Error ${response.status}: ${response.text}` };
yield { type: "error", error: `API Error ${response.status}: ${err}` };
return; return;
} }
const reader = response.body?.getReader(); const text: string = response.json.content?.[0]?.text ?? "";
if (!reader) { yield { type: "text", text };
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
}
}
}
yield { type: "done" }; yield { type: "done" };
} }
/** Non-streaming version — uses Obsidian's requestUrl to bypass CORS */ /** Non-streaming convenience wrapper */
async chat(messages: ClaudeMessage[], options: ClaudeOptions): Promise<string> { async chat(messages: ClaudeMessage[], options: ClaudeOptions): Promise<string> {
const response = await requestUrl({ const response = await requestUrl({
url: this.baseUrl, url: this.baseUrl,