fix: true SSE streaming via fetch instead of requestUrl
Replaces the buffered requestUrl call with native fetch + ReadableStream so text chunks are yielded incrementally as they arrive. The ChatView streaming loop is unchanged — it already handles chunks correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32728,30 +32728,65 @@ var ClaudeClient = class {
|
||||
"anthropic-version": "2023-06-01"
|
||||
};
|
||||
}
|
||||
/**
|
||||
* "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.
|
||||
*/
|
||||
/** Stream a chat completion via fetch + SSE, yielding text chunks as they arrive. */
|
||||
async *streamChat(messages, options) {
|
||||
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: options.maxTokens ?? 8192,
|
||||
system: options.systemPrompt,
|
||||
messages
|
||||
}),
|
||||
throw: false
|
||||
});
|
||||
if (response.status >= 400) {
|
||||
yield { type: "error", error: `API Error ${response.status}: ${response.text}` };
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(this.baseUrl, {
|
||||
method: "POST",
|
||||
headers: this.headers(options.apiKey),
|
||||
body: JSON.stringify({
|
||||
model: options.model,
|
||||
max_tokens: options.maxTokens ?? 8192,
|
||||
system: options.systemPrompt,
|
||||
messages,
|
||||
stream: true
|
||||
})
|
||||
});
|
||||
} catch (e) {
|
||||
yield { type: "error", error: e.message };
|
||||
return;
|
||||
}
|
||||
const text = response.json.content?.[0]?.text ?? "";
|
||||
yield { type: "text", text };
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
yield { type: "error", error: `API Error ${response.status}: ${text}` };
|
||||
return;
|
||||
}
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
yield { type: "error", error: "No response body" };
|
||||
return;
|
||||
}
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
try {
|
||||
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 event = JSON.parse(data);
|
||||
if (event.type === "content_block_delta" && event.delta?.type === "text_delta") {
|
||||
yield { type: "text", text: event.delta.text };
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
yield { type: "done" };
|
||||
}
|
||||
/** Non-streaming convenience wrapper */
|
||||
|
||||
+59
-23
@@ -18,7 +18,7 @@ export interface ClaudeStreamChunk {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Minimal Claude API client using Obsidian's requestUrl (bypasses CORS) */
|
||||
/** Minimal Claude API client. streamChat uses fetch+SSE; other methods use requestUrl. */
|
||||
export class ClaudeClient {
|
||||
private baseUrl = "https://api.anthropic.com/v1/messages";
|
||||
|
||||
@@ -30,35 +30,71 @@ export class ClaudeClient {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* "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.
|
||||
*/
|
||||
/** Stream a chat completion via fetch + SSE, yielding text chunks as they arrive. */
|
||||
async *streamChat(
|
||||
messages: ClaudeMessage[],
|
||||
options: ClaudeOptions
|
||||
): AsyncGenerator<ClaudeStreamChunk> {
|
||||
const response = await requestUrl({
|
||||
url: this.baseUrl,
|
||||
method: "POST",
|
||||
headers: this.headers(options.apiKey),
|
||||
body: JSON.stringify({
|
||||
model: options.model,
|
||||
max_tokens: options.maxTokens ?? 8192,
|
||||
system: options.systemPrompt,
|
||||
messages,
|
||||
}),
|
||||
throw: false,
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
yield { type: "error", error: `API Error ${response.status}: ${response.text}` };
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(this.baseUrl, {
|
||||
method: "POST",
|
||||
headers: this.headers(options.apiKey),
|
||||
body: JSON.stringify({
|
||||
model: options.model,
|
||||
max_tokens: options.maxTokens ?? 8192,
|
||||
system: options.systemPrompt,
|
||||
messages,
|
||||
stream: true,
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
yield { type: "error", error: (e as Error).message };
|
||||
return;
|
||||
}
|
||||
|
||||
const text: string = response.json.content?.[0]?.text ?? "";
|
||||
yield { type: "text", text };
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
yield { type: "error", error: `API Error ${response.status}: ${text}` };
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
yield { type: "error", error: "No response body" };
|
||||
return;
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
try {
|
||||
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 event = JSON.parse(data);
|
||||
if (event.type === "content_block_delta" && event.delta?.type === "text_delta") {
|
||||
yield { type: "text", text: event.delta.text };
|
||||
}
|
||||
} catch {
|
||||
// skip malformed SSE lines
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
|
||||
yield { type: "done" };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user