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:
@@ -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
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user