Skip to content

Commit 98837c4

Browse files
committed
minor issues and update to library
1 parent 10a82aa commit 98837c4

File tree

4 files changed

+173
-30
lines changed

4 files changed

+173
-30
lines changed

front_end/panels/ai_chat/LLM/AnthropicProvider.ts

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const logger = createLogger('AnthropicProvider');
1717
export class AnthropicProvider extends LLMBaseProvider {
1818
private static readonly API_BASE_URL = 'https://api.anthropic.com/v1';
1919
private static readonly MESSAGES_PATH = '/messages';
20+
private static readonly MODELS_PATH = '/models';
2021
private static readonly API_VERSION = '2023-06-01';
2122

2223
readonly name: LLMProvider = 'anthropic';
@@ -32,6 +33,13 @@ export class AnthropicProvider extends LLMBaseProvider {
3233
return `${AnthropicProvider.API_BASE_URL}${AnthropicProvider.MESSAGES_PATH}`;
3334
}
3435

36+
/**
37+
* Get the models endpoint URL
38+
*/
39+
private getModelsEndpoint(): string {
40+
return `${AnthropicProvider.API_BASE_URL}${AnthropicProvider.MODELS_PATH}`;
41+
}
42+
3543
/**
3644
* Convert MessageContent to Anthropic format
3745
*/
@@ -165,6 +173,7 @@ export class AnthropicProvider extends LLMBaseProvider {
165173
'Content-Type': 'application/json',
166174
'x-api-key': this.apiKey,
167175
'anthropic-version': AnthropicProvider.API_VERSION,
176+
'anthropic-dangerous-direct-browser-access': 'true',
168177
};
169178

170179
// Add beta headers if provided
@@ -322,13 +331,95 @@ export class AnthropicProvider extends LLMBaseProvider {
322331
return LLMResponseParser.parseResponse(response);
323332
}
324333

334+
/**
335+
* Fetch available models from Anthropic API
336+
*/
337+
async fetchModels(apiKey?: string, endpoint?: string): Promise<AnthropicModel[]> {
338+
logger.debug('Fetching available Anthropic models...');
339+
340+
// Use provided apiKey if available, otherwise fall back to instance apiKey
341+
const keyToUse = apiKey || this.apiKey;
342+
343+
try {
344+
const response = await fetch(this.getModelsEndpoint(), {
345+
method: 'GET',
346+
headers: {
347+
'x-api-key': keyToUse,
348+
'anthropic-version': AnthropicProvider.API_VERSION,
349+
'anthropic-dangerous-direct-browser-access': 'true',
350+
},
351+
});
352+
353+
if (!response.ok) {
354+
const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } }));
355+
logger.error('Anthropic models API error:', JSON.stringify(errorData, null, 2));
356+
throw new Error(`Anthropic models API error: ${response.statusText} - ${errorData?.error?.message || 'Unknown error'}`);
357+
}
358+
359+
const data: AnthropicModelsResponse = await response.json();
360+
logger.debug('Anthropic Models Response:', data);
361+
362+
if (!data?.data || !Array.isArray(data.data)) {
363+
throw new Error('Invalid models response format');
364+
}
365+
366+
return data.data;
367+
} catch (error) {
368+
logger.error('Failed to fetch Anthropic models:', error);
369+
throw error;
370+
}
371+
}
372+
325373
/**
326374
* Get all models supported by this provider
327375
*/
328376
async getModels(): Promise<ModelInfo[]> {
329-
// Anthropic doesn't provide a public models API endpoint
330-
// Return hardcoded list of known models
331-
return this.getDefaultModels();
377+
try {
378+
// Fetch models from Anthropic API
379+
const anthropicModels = await this.fetchModels();
380+
381+
return anthropicModels.map(model => ({
382+
id: model.id,
383+
name: model.display_name || model.id,
384+
provider: 'anthropic' as LLMProvider,
385+
capabilities: {
386+
functionCalling: this.modelSupportsFunctionCalling(model.id),
387+
reasoning: this.modelSupportsReasoning(model.id),
388+
vision: this.modelSupportsVision(model.id),
389+
structured: true, // All Anthropic models support structured outputs
390+
}
391+
}));
392+
} catch (error) {
393+
logger.warn('Failed to fetch models from API, falling back to default list:', error);
394+
// Fallback to hardcoded list if API call fails
395+
return this.getDefaultModels();
396+
}
397+
}
398+
399+
/**
400+
* Check if a model supports function calling
401+
*/
402+
private modelSupportsFunctionCalling(modelId: string): boolean {
403+
// All Claude 3+ models support function calling
404+
return modelId.includes('claude-3') || modelId.includes('claude-sonnet-4');
405+
}
406+
407+
/**
408+
* Check if a model supports extended thinking/reasoning
409+
*/
410+
private modelSupportsReasoning(modelId: string): boolean {
411+
// Claude Sonnet 4.5 and newer support extended thinking
412+
return modelId.includes('claude-sonnet-4.5') || modelId.includes('claude-sonnet-4-');
413+
}
414+
415+
/**
416+
* Check if a model supports vision/image inputs
417+
*/
418+
private modelSupportsVision(modelId: string): boolean {
419+
// Claude 3 Opus, Sonnet, and Haiku support vision
420+
// Claude Sonnet 4+ also supports vision
421+
return (modelId.includes('claude-3') && !modelId.includes('haiku')) ||
422+
modelId.includes('claude-sonnet-4');
332423
}
333424

334425
/**
@@ -467,3 +558,23 @@ export class AnthropicProvider extends LLMBaseProvider {
467558
};
468559
}
469560
}
561+
562+
/**
563+
* Anthropic model object from the /v1/models API
564+
*/
565+
interface AnthropicModel {
566+
id: string;
567+
display_name: string;
568+
created_at: string;
569+
type: 'model';
570+
}
571+
572+
/**
573+
* Response from Anthropic /v1/models endpoint
574+
*/
575+
interface AnthropicModelsResponse {
576+
data: AnthropicModel[];
577+
first_id: string;
578+
last_id: string;
579+
has_more: boolean;
580+
}

front_end/panels/ai_chat/LLM/GoogleAIProvider.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,11 +344,17 @@ export class GoogleAIProvider extends LLMBaseProvider {
344344
/**
345345
* Fetch available models from Google AI API
346346
*/
347-
async fetchModels(): Promise<GoogleAIModel[]> {
347+
async fetchModels(apiKey?: string, endpoint?: string): Promise<GoogleAIModel[]> {
348348
logger.debug('Fetching available Google AI models...');
349349

350+
// Use provided apiKey if available, otherwise fall back to instance apiKey
351+
const keyToUse = apiKey || this.apiKey;
352+
353+
// Build endpoint URL with API key
354+
const modelsUrl = `${GoogleAIProvider.API_BASE_URL}/models?key=${keyToUse}`;
355+
350356
try {
351-
const response = await fetch(this.getModelsEndpoint(), {
357+
const response = await fetch(modelsUrl, {
352358
method: 'GET',
353359
});
354360

front_end/panels/ai_chat/ui/CustomProviderDialog.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44

55
import * as UI from '../../../ui/legacy/legacy.js';
66
import * as Geometry from '../../../models/geometry/geometry.js';
7-
import { CustomProviderManager } from '../core/CustomProviderManager.js';
8-
import type { CustomProviderConfig } from '../core/CustomProviderManager.js';
7+
import { CustomProviderManager, type CustomProviderConfig } from '../core/CustomProviderManager.js';
98
import { LLMClient } from '../LLM/LLMClient.js';
109
import { createLogger } from '../core/Logger.js';
1110
import { PROVIDER_SELECTION_KEY } from './settings/constants.js';

front_end/panels/ai_chat/vendor/readability-source.ts

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
// found in the LICENSE file.
44

55
/**
6-
* Mozilla Readability library bundled as a string for runtime injection.
7-
* Version: 0.6.0
6+
* Bundled Mozilla Readability library (main branch)
87
* Source: https://github.com/mozilla/readability
9-
* License: Apache-2.0
8+
* This file contains the Readability.js source code as a string constant
9+
* for injection into web pages for content extraction.
1010
*/
1111

12-
// @ts-nocheck - Large bundled library source
13-
export const READABILITY_SOURCE = `/*
12+
export const READABILITY_SOURCE = `
13+
/*
1414
* Copyright (c) 2010 Arc90 Inc
1515
*
1616
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -151,7 +151,8 @@ Readability.prototype = {
151151
// Readability-readerable.js. Please keep both copies in sync.
152152
unlikelyCandidates:
153153
/-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,
154-
okMaybeItsACandidate: /and|article|body|column|content|main|shadow/i,
154+
okMaybeItsACandidate:
155+
/and|article|body|column|content|main|mathjax|shadow/i,
155156
156157
positive:
157158
/article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i,
@@ -163,7 +164,7 @@ Readability.prototype = {
163164
replaceFonts: /<(\\/?)font[^>]*>/gi,
164165
normalize: /\\s{2,}/g,
165166
videos:
166-
/\\/\\/(www\\.)?((dailymotion|youtube|youtube-nocookie|player\\.vimeo|v\\.qq)\\.com|(archive|upload\\.wikimedia)\\.org|player\\.twitch\\.tv)/i,
167+
/\\/\\/(www\\.)?((dailymotion|youtube|youtube-nocookie|player\\.vimeo|v\\.qq|bilibili|live.bilibili)\\.com|(archive|upload\\.wikimedia)\\.org|player\\.twitch\\.tv)/i,
167168
shareElements: /(\\b|_)(share|sharedaddy)(\\b|_)/i,
168169
nextLink: /(next|weiter|continue|>([^\\|]|$)|»([^\\|]|$))/i,
169170
prevLink: /(prev|earl|old|new|<|«)/i,
@@ -605,14 +606,20 @@ Readability.prototype = {
605606
}
606607
607608
// If there's a separator in the title, first remove the final part
608-
if (/ [\\|\\-\\\\\\/>»] /.test(curTitle)) {
609-
titleHadHierarchicalSeparators = / [\\\\\\/>»] /.test(curTitle);
610-
let allSeparators = Array.from(origTitle.matchAll(/ [\\|\\-\\\\\\/>»] /gi));
609+
const titleSeparators = /\\|\\-–—\\\\\\/>»/.source;
610+
if (new RegExp(\`\\\\s[\${titleSeparators}]\\\\s\`).test(curTitle)) {
611+
titleHadHierarchicalSeparators = /\\s[\\\\\\/>»]\\s/.test(curTitle);
612+
let allSeparators = Array.from(
613+
origTitle.matchAll(new RegExp(\`\\\\s[\${titleSeparators}]\\\\s\`, "gi"))
614+
);
611615
curTitle = origTitle.substring(0, allSeparators.pop().index);
612616
613617
// If the resulting title is too short, remove the first part instead:
614618
if (wordCount(curTitle) < 3) {
615-
curTitle = origTitle.replace(/^[^\\|\\-\\\\\\/>»]*[\\|\\-\\\\\\/>»]/gi, "");
619+
curTitle = origTitle.replace(
620+
new RegExp(\`^[^\${titleSeparators}]*[\${titleSeparators}]\`, "gi"),
621+
""
622+
);
616623
}
617624
} else if (curTitle.includes(": ")) {
618625
// Check if we have an heading containing this exact string, so we
@@ -654,7 +661,10 @@ Readability.prototype = {
654661
curTitleWordCount <= 4 &&
655662
(!titleHadHierarchicalSeparators ||
656663
curTitleWordCount !=
657-
wordCount(origTitle.replace(/[\\|\\-\\\\\\/>»]+/g, "")) - 1)
664+
wordCount(
665+
origTitle.replace(new RegExp(\`\\\\s[\${titleSeparators}]\\\\s\`, "g"), "")
666+
) -
667+
1)
658668
) {
659669
curTitle = origTitle;
660670
}
@@ -1176,23 +1186,39 @@ Readability.prototype = {
11761186
// Turn all divs that don't have children block level elements into p's
11771187
if (node.tagName === "DIV") {
11781188
// Put phrasing content into paragraphs.
1179-
var p = null;
11801189
var childNode = node.firstChild;
11811190
while (childNode) {
11821191
var nextSibling = childNode.nextSibling;
11831192
if (this._isPhrasingContent(childNode)) {
1184-
if (p !== null) {
1185-
p.appendChild(childNode);
1186-
} else if (!this._isWhitespace(childNode)) {
1187-
p = doc.createElement("p");
1188-
node.replaceChild(p, childNode);
1189-
p.appendChild(childNode);
1193+
var fragment = doc.createDocumentFragment();
1194+
// Collect all consecutive phrasing content into a fragment.
1195+
do {
1196+
nextSibling = childNode.nextSibling;
1197+
fragment.appendChild(childNode);
1198+
childNode = nextSibling;
1199+
} while (childNode && this._isPhrasingContent(childNode));
1200+
1201+
// Trim leading and trailing whitespace from the fragment.
1202+
while (
1203+
fragment.firstChild &&
1204+
this._isWhitespace(fragment.firstChild)
1205+
) {
1206+
fragment.firstChild.remove();
11901207
}
1191-
} else if (p !== null) {
1192-
while (p.lastChild && this._isWhitespace(p.lastChild)) {
1193-
p.lastChild.remove();
1208+
while (
1209+
fragment.lastChild &&
1210+
this._isWhitespace(fragment.lastChild)
1211+
) {
1212+
fragment.lastChild.remove();
1213+
}
1214+
1215+
// If the fragment contains anything, wrap it in a paragraph and
1216+
// insert it before the next non-phrasing node.
1217+
if (fragment.firstChild) {
1218+
var p = doc.createElement("p");
1219+
p.appendChild(fragment);
1220+
node.insertBefore(p, nextSibling);
11941221
}
1195-
p = null;
11961222
}
11971223
childNode = nextSibling;
11981224
}
@@ -2796,4 +2822,5 @@ if (typeof module === "object") {
27962822
/* global module */
27972823
module.exports = Readability;
27982824
}
2825+
27992826
`;

0 commit comments

Comments
 (0)