storkit: merge 90_story_fetch_real_context_window_size_from_anthropic_models_api
This commit is contained in:
@@ -115,6 +115,11 @@ export interface Message {
|
|||||||
tool_call_id?: string;
|
tool_call_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AnthropicModelInfo {
|
||||||
|
id: string;
|
||||||
|
context_window: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WorkItemContent {
|
export interface WorkItemContent {
|
||||||
content: string;
|
content: string;
|
||||||
stage: string;
|
stage: string;
|
||||||
@@ -266,7 +271,7 @@ export const api = {
|
|||||||
return requestJson<boolean>("/anthropic/key/exists", {}, baseUrl);
|
return requestJson<boolean>("/anthropic/key/exists", {}, baseUrl);
|
||||||
},
|
},
|
||||||
getAnthropicModels(baseUrl?: string) {
|
getAnthropicModels(baseUrl?: string) {
|
||||||
return requestJson<string[]>("/anthropic/models", {}, baseUrl);
|
return requestJson<AnthropicModelInfo[]>("/anthropic/models", {}, baseUrl);
|
||||||
},
|
},
|
||||||
setAnthropicApiKey(api_key: string, baseUrl?: string) {
|
setAnthropicApiKey(api_key: string, baseUrl?: string) {
|
||||||
return requestJson<boolean>(
|
return requestJson<boolean>(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
|||||||
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||||
import type { AgentConfigInfo } from "../api/agents";
|
import type { AgentConfigInfo } from "../api/agents";
|
||||||
import { agentsApi } from "../api/agents";
|
import { agentsApi } from "../api/agents";
|
||||||
import type { PipelineState } from "../api/client";
|
import type { AnthropicModelInfo, PipelineState } from "../api/client";
|
||||||
import { api, ChatWebSocket } from "../api/client";
|
import { api, ChatWebSocket } from "../api/client";
|
||||||
import { useChatHistory } from "../hooks/useChatHistory";
|
import { useChatHistory } from "../hooks/useChatHistory";
|
||||||
import type { Message, ProviderConfig } from "../types";
|
import type { Message, ProviderConfig } from "../types";
|
||||||
@@ -143,8 +143,13 @@ function formatToolActivity(toolName: string): string {
|
|||||||
|
|
||||||
const estimateTokens = (text: string): number => Math.ceil(text.length / 4);
|
const estimateTokens = (text: string): number => Math.ceil(text.length / 4);
|
||||||
|
|
||||||
const getContextWindowSize = (modelName: string): number => {
|
const getContextWindowSize = (
|
||||||
if (modelName.startsWith("claude-")) return 200000;
|
modelName: string,
|
||||||
|
claudeContextWindows?: Map<string, number>,
|
||||||
|
): number => {
|
||||||
|
if (modelName.startsWith("claude-")) {
|
||||||
|
return claudeContextWindows?.get(modelName) ?? 200000;
|
||||||
|
}
|
||||||
if (modelName.includes("llama3")) return 8192;
|
if (modelName.includes("llama3")) return 8192;
|
||||||
if (modelName.includes("qwen2.5")) return 32768;
|
if (modelName.includes("qwen2.5")) return 32768;
|
||||||
if (modelName.includes("deepseek")) return 16384;
|
if (modelName.includes("deepseek")) return 16384;
|
||||||
@@ -163,6 +168,9 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
const [enableTools, setEnableTools] = useState(true);
|
const [enableTools, setEnableTools] = useState(true);
|
||||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||||
const [claudeModels, setClaudeModels] = useState<string[]>([]);
|
const [claudeModels, setClaudeModels] = useState<string[]>([]);
|
||||||
|
const [claudeContextWindowMap, setClaudeContextWindowMap] = useState<
|
||||||
|
Map<string, number>
|
||||||
|
>(new Map());
|
||||||
const [streamingContent, setStreamingContent] = useState("");
|
const [streamingContent, setStreamingContent] = useState("");
|
||||||
const [streamingThinking, setStreamingThinking] = useState("");
|
const [streamingThinking, setStreamingThinking] = useState("");
|
||||||
const [showApiKeyDialog, setShowApiKeyDialog] = useState(false);
|
const [showApiKeyDialog, setShowApiKeyDialog] = useState(false);
|
||||||
@@ -285,7 +293,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
totalTokens += estimateTokens(streamingContent);
|
totalTokens += estimateTokens(streamingContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
const contextWindow = getContextWindowSize(model);
|
const contextWindow = getContextWindowSize(model, claudeContextWindowMap);
|
||||||
const percentage = Math.round((totalTokens / contextWindow) * 100);
|
const percentage = Math.round((totalTokens / contextWindow) * 100);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -293,7 +301,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
total: contextWindow,
|
total: contextWindow,
|
||||||
percentage,
|
percentage,
|
||||||
};
|
};
|
||||||
}, [messages, streamingContent, model]);
|
}, [messages, streamingContent, model, claudeContextWindowMap]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
@@ -337,14 +345,18 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
.then((exists) => {
|
.then((exists) => {
|
||||||
setHasAnthropicKey(exists);
|
setHasAnthropicKey(exists);
|
||||||
if (!exists) return;
|
if (!exists) return;
|
||||||
return api.getAnthropicModels().then((models) => {
|
return api.getAnthropicModels().then((models: AnthropicModelInfo[]) => {
|
||||||
if (models.length > 0) {
|
if (models.length > 0) {
|
||||||
const sortedModels = models.sort((a, b) =>
|
const sortedModels = models.sort((a, b) =>
|
||||||
a.toLowerCase().localeCompare(b.toLowerCase()),
|
a.id.toLowerCase().localeCompare(b.id.toLowerCase()),
|
||||||
|
);
|
||||||
|
setClaudeModels(sortedModels.map((m) => m.id));
|
||||||
|
setClaudeContextWindowMap(
|
||||||
|
new Map(sortedModels.map((m) => [m.id, m.context_window])),
|
||||||
);
|
);
|
||||||
setClaudeModels(sortedModels);
|
|
||||||
} else {
|
} else {
|
||||||
setClaudeModels([]);
|
setClaudeModels([]);
|
||||||
|
setClaudeContextWindowMap(new Map());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::llm::chat;
|
|||||||
use crate::store::StoreOps;
|
use crate::store::StoreOps;
|
||||||
use poem_openapi::{Object, OpenApi, Tags, payload::Json};
|
use poem_openapi::{Object, OpenApi, Tags, payload::Json};
|
||||||
use reqwest::header::{HeaderMap, HeaderValue};
|
use reqwest::header::{HeaderMap, HeaderValue};
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
const ANTHROPIC_MODELS_URL: &str = "https://api.anthropic.com/v1/models";
|
const ANTHROPIC_MODELS_URL: &str = "https://api.anthropic.com/v1/models";
|
||||||
@@ -18,6 +18,13 @@ struct AnthropicModelsResponse {
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct AnthropicModelInfo {
|
struct AnthropicModelInfo {
|
||||||
id: String,
|
id: String,
|
||||||
|
context_window: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Object)]
|
||||||
|
struct AnthropicModelSummary {
|
||||||
|
id: String,
|
||||||
|
context_window: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_anthropic_api_key(ctx: &AppContext) -> Result<String, String> {
|
fn get_anthropic_api_key(ctx: &AppContext) -> Result<String, String> {
|
||||||
@@ -84,7 +91,7 @@ impl AnthropicApi {
|
|||||||
|
|
||||||
/// List available Anthropic models.
|
/// List available Anthropic models.
|
||||||
#[oai(path = "/anthropic/models", method = "get")]
|
#[oai(path = "/anthropic/models", method = "get")]
|
||||||
async fn list_anthropic_models(&self) -> OpenApiResult<Json<Vec<String>>> {
|
async fn list_anthropic_models(&self) -> OpenApiResult<Json<Vec<AnthropicModelSummary>>> {
|
||||||
self.list_anthropic_models_from(ANTHROPIC_MODELS_URL).await
|
self.list_anthropic_models_from(ANTHROPIC_MODELS_URL).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,7 +100,7 @@ impl AnthropicApi {
|
|||||||
async fn list_anthropic_models_from(
|
async fn list_anthropic_models_from(
|
||||||
&self,
|
&self,
|
||||||
url: &str,
|
url: &str,
|
||||||
) -> OpenApiResult<Json<Vec<String>>> {
|
) -> OpenApiResult<Json<Vec<AnthropicModelSummary>>> {
|
||||||
let api_key = get_anthropic_api_key(self.ctx.as_ref()).map_err(bad_request)?;
|
let api_key = get_anthropic_api_key(self.ctx.as_ref()).map_err(bad_request)?;
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
@@ -128,7 +135,14 @@ impl AnthropicApi {
|
|||||||
.json::<AnthropicModelsResponse>()
|
.json::<AnthropicModelsResponse>()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| bad_request(e.to_string()))?;
|
.map_err(|e| bad_request(e.to_string()))?;
|
||||||
let models = body.data.into_iter().map(|m| m.id).collect();
|
let models = body
|
||||||
|
.data
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| AnthropicModelSummary {
|
||||||
|
id: m.id,
|
||||||
|
context_window: m.context_window,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(Json(models))
|
Ok(Json(models))
|
||||||
}
|
}
|
||||||
@@ -276,4 +290,29 @@ mod tests {
|
|||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
let _api = make_api(&dir);
|
let _api = make_api(&dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn anthropic_model_info_deserializes_context_window() {
|
||||||
|
let json = json!({
|
||||||
|
"id": "claude-opus-4-5",
|
||||||
|
"context_window": 200000
|
||||||
|
});
|
||||||
|
let info: AnthropicModelInfo = serde_json::from_value(json).unwrap();
|
||||||
|
assert_eq!(info.id, "claude-opus-4-5");
|
||||||
|
assert_eq!(info.context_window, 200000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn anthropic_models_response_deserializes_multiple_models() {
|
||||||
|
let json = json!({
|
||||||
|
"data": [
|
||||||
|
{ "id": "claude-opus-4-5", "context_window": 200000 },
|
||||||
|
{ "id": "claude-haiku-4-5-20251001", "context_window": 100000 }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
let response: AnthropicModelsResponse = serde_json::from_value(json).unwrap();
|
||||||
|
assert_eq!(response.data.len(), 2);
|
||||||
|
assert_eq!(response.data[0].context_window, 200000);
|
||||||
|
assert_eq!(response.data[1].context_window, 100000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user