@brandon_9527/tcode 1.0.6 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/python-src/main.py +2 -1
- package/dist/python-src/skill_agent.py +144 -0
- package/dist/python-src/src/agents/token_tracker.py +44 -0
- package/dist/python-src/src/claw/__init__.py +0 -0
- package/dist/python-src/src/claw/bus/__init__.py +3 -0
- package/dist/python-src/src/claw/bus/events.py +10 -0
- package/dist/python-src/src/claw/bus/queue.py +43 -0
- package/dist/python-src/src/claw/channels/__init__.py +3 -0
- package/dist/python-src/src/claw/channels/base.py +30 -0
- package/dist/python-src/src/claw/channels/feishu.py +89 -0
- package/dist/python-src/src/claw/channels/manager.py +47 -0
- package/dist/python-src/src/claw/config/schema.py +46 -0
- package/dist/python-src/src/managers/manager_agent.py +2 -2
- package/dist/python-src/src/managers/manager_instruction.py +7 -7
- package/dist/python-src/src/managers/manager_skill.py +121 -0
- package/dist/python-src/src/managers/sandbox.py +3 -3
- package/dist/python-src/src/middlewares/dynamic_content.py +3 -3
- package/dist/python-src/src/middlewares/hitl.py +3 -3
- package/dist/python-src/src/middlewares/memory.py +2 -2
- package/dist/python-src/src/middlewares/skill.py +27 -0
- package/dist/python-src/src/middlewares/subagents.py +2 -2
- package/dist/python-src/src/middlewares/summary.py +37 -37
- package/dist/python-src/src/stream/formatter.py +19 -19
- package/dist/python-src/src/trackers/__init__.py +0 -0
- package/dist/python-src/src/trackers/token/__init__.py +0 -0
- package/dist/python-src/src/trackers/token/cli.py +45 -0
- package/dist/python-src/src/trackers/token/pricing.py +39 -0
- package/dist/python-src/src/trackers/token/report.py +114 -0
- package/dist/python-src/src/trackers/token/tracker.py +65 -0
- package/dist/python-src/src/tui/chatui.py +11 -10
- package/dist/python-src/src/tui/components/tlist.py +5 -5
- package/dist/python-src/src/tui/components/tscroll_panel.py +14 -14
- package/dist/python-src/src/tui/utils/trender.py +23 -22
- package/package.json +1 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
_C='managed'
|
|
2
|
+
_B='user'
|
|
3
|
+
_A='project'
|
|
4
|
+
from typing import Optional,Dict,Any,List,Tuple
|
|
5
|
+
from dataclasses import dataclass,field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import logging,yaml,re,os
|
|
8
|
+
logger=logging.getLogger(__name__)
|
|
9
|
+
@dataclass
|
|
10
|
+
class Skill:
|
|
11
|
+
name:str;description:str;content:str;path:Path;license:Optional[str]=None;allowed_tools:List[str]=field(default_factory=list);metadata:Dict[str,Any]=field(default_factory=dict);location:str=_A
|
|
12
|
+
@classmethod
|
|
13
|
+
def from_skill_md(C,skill_md_path,location=_A):
|
|
14
|
+
B=skill_md_path
|
|
15
|
+
if not B.exists():return
|
|
16
|
+
F=B.read_text(encoding='utf-8');A,G=C.br(F)
|
|
17
|
+
if not A:return
|
|
18
|
+
D=A.get('name');E=A.get('description')
|
|
19
|
+
if not D or not E:return
|
|
20
|
+
return C(name=D,description=E,content=G.strip(),path=B.parent,license=A.get('license'),allowed_tools=A.get('allowed-tools',[])or[],metadata=A.get('metadata',{})or{},location=location)
|
|
21
|
+
@staticmethod
|
|
22
|
+
def br(content):
|
|
23
|
+
A=content;C='^---\\s*\\n(.*?)\\n---\\s*\\n(.*)$';B=re.match(C,A,re.DOTALL)
|
|
24
|
+
if not B:return{},A
|
|
25
|
+
D=B.group(1);E=B.group(2)
|
|
26
|
+
try:F=yaml.safe_load(D)or{}
|
|
27
|
+
except yaml.YAMLError:return{},A
|
|
28
|
+
return F,E
|
|
29
|
+
def get_prompt(A):B=f"Loading: {A.name} \nBase directory: {A.path}\n \n ";return B+A.content
|
|
30
|
+
def to_xml(A):return f"<skill>\n<name>{A.name}</name>\n<description>{A.description}</description>\n<location>{A.location}</location>\n</skill>"
|
|
31
|
+
def __repr__(A):return f"Skill(name={A.name!r}, location={A.location!r})"
|
|
32
|
+
class SkillManager:
|
|
33
|
+
SKILL_DIRS=['.claude/skills','.autodev/skills'];SKILL_FILE='SKILL.md'
|
|
34
|
+
def __init__(A,project_root=None,home_path=Path.home()):C=home_path;B=project_root;A._skills={};A._skills_by_location={_A:[],_B:[],_C:[]};A.project_root=Path(B)if B else Path.cwd();A.home_dir=Path(C)if C else Path.home()
|
|
35
|
+
def register(B,skill):
|
|
36
|
+
A=skill;C=B._skills.get(A.name)
|
|
37
|
+
if C:
|
|
38
|
+
D={_A:0,_B:1,_C:2};E=D.get(C.location,99);F=D.get(A.location,99)
|
|
39
|
+
if F>=E:return False
|
|
40
|
+
B._skills[A.name]=A;B._skills_by_location[A.location].append(A);return True
|
|
41
|
+
def get(A,name):return A._skills.get(name)
|
|
42
|
+
def exists(A,name):return name in A._skills
|
|
43
|
+
def list_all(A):return list(A._skills.values())
|
|
44
|
+
def list_by_location(A,location):return A._skills_by_location.get(location,[])
|
|
45
|
+
def clear(A):
|
|
46
|
+
A._skills.clear()
|
|
47
|
+
for B in A._skills_by_location:A._skills_by_location[B].clear()
|
|
48
|
+
def generate_skills_prompt(E,char_budget=10000):
|
|
49
|
+
C=E.list_all()
|
|
50
|
+
if not C:return'Execute a skill within the main conversation.\n\nNo skills are currently available. Skills can be added to:\n- .claude/skills/ (project-level)\n- ~/.claude/skills/ (user-level)\n- .minion/skills/ (project-level)\n- ~/.minion/skills/ (user-level) \n '
|
|
51
|
+
A=[];D=0
|
|
52
|
+
for F in C:
|
|
53
|
+
B=F.to_xml()
|
|
54
|
+
if D+len(B)>char_budget:break
|
|
55
|
+
A.append(B);D+=len(B)
|
|
56
|
+
if not A:return'Execute a skill within the main conversation.\n\nNo skills are currently available. Skills can be added to:\n- .claude/skills/ (project-level)\n- ~/.claude/skills/ (user-level)\n- .minion/skills/ (project-level)\n- ~/.minion/skills/ (user-level) \n'
|
|
57
|
+
G='\n'.join(A);return f'''Execute a skill within the main conversation.
|
|
58
|
+
|
|
59
|
+
<skills_instructions>
|
|
60
|
+
When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.
|
|
61
|
+
|
|
62
|
+
How to use skills:
|
|
63
|
+
- Invoke skills using skill tool with the skill name only (no arguments)
|
|
64
|
+
- When you invoke a skill, you will see <command-message>The "{{name}}" skill is loading</command-message>
|
|
65
|
+
- The skill\'s prompt will expand and provide detailed instructions on how to complete the task
|
|
66
|
+
- Base directory provided in output for resolving bundled resources (references/, scripts/, assets/)
|
|
67
|
+
|
|
68
|
+
Important:
|
|
69
|
+
- Only use skills listed in <available_skills> below
|
|
70
|
+
- Do not invoke a skill that is already running
|
|
71
|
+
</skills_instructions>
|
|
72
|
+
|
|
73
|
+
<available_skills>
|
|
74
|
+
{G}
|
|
75
|
+
</available_skills>'''
|
|
76
|
+
def __len__(A):return len(A._skills)
|
|
77
|
+
def __contains__(A,name):return name in A._skills
|
|
78
|
+
def __iter__(A):return iter(A._skills.values())
|
|
79
|
+
def get_search_paths(A):
|
|
80
|
+
B=[]
|
|
81
|
+
for C in A.SKILL_DIRS:D=A.project_root/C;B.append((D,_A))
|
|
82
|
+
for C in A.SKILL_DIRS:E=A.home_dir/C;B.append((E,_B))
|
|
83
|
+
return B
|
|
84
|
+
def discover_skills(D,skills_dir):
|
|
85
|
+
A=skills_dir
|
|
86
|
+
if not A.exists()or not A.is_dir():return[]
|
|
87
|
+
B=[]
|
|
88
|
+
for C in A.iterdir():
|
|
89
|
+
if C.is_dir():
|
|
90
|
+
E=C/D.SKILL_FILE
|
|
91
|
+
if E.exists():B.append(E)
|
|
92
|
+
else:
|
|
93
|
+
for F in C.iterdir():
|
|
94
|
+
if F.is_dir():
|
|
95
|
+
G=F/D.SKILL_FILE
|
|
96
|
+
if G.exists():B.append(G)
|
|
97
|
+
return B
|
|
98
|
+
def load_skill(D,skill_md_path,location):
|
|
99
|
+
A=skill_md_path
|
|
100
|
+
try:
|
|
101
|
+
B=Skill.from_skill_md(A,location)
|
|
102
|
+
if B:logger.debug(f"Loaded skill: {B.name} from {A}")
|
|
103
|
+
else:logger.warning(f"Failed to parse skill: {A}")
|
|
104
|
+
return B
|
|
105
|
+
except Exception as C:logger.error(f"Error loading skill from {A}: {C}");return
|
|
106
|
+
def load_all(A):
|
|
107
|
+
for(D,C)in A.get_search_paths():
|
|
108
|
+
E=A.discover_skills(D)
|
|
109
|
+
for F in E:
|
|
110
|
+
B=A.load_skill(F,C)
|
|
111
|
+
if B:
|
|
112
|
+
G=A.register(B)
|
|
113
|
+
if G:logger.info(f"Registered skill: {B.name} ({C})")
|
|
114
|
+
else:logger.debug(f"Skipped skill {B.name} - already registered from high priority location")
|
|
115
|
+
return A
|
|
116
|
+
def reload(A):A.clear();return A.load_all()
|
|
117
|
+
def main():
|
|
118
|
+
A=SkillManager(project_root=Path.cwd());A.load_all();C=A.list_all();print(f"Loaded {len(C)} skills");B=A.get('arxiv-search')
|
|
119
|
+
if B:print(B.get_prompt())
|
|
120
|
+
D=A.generate_skills_prompt(char_budget=5000);print(D);A.reload()
|
|
121
|
+
if __name__=='__main__':main()
|
|
@@ -21,14 +21,14 @@ sys.path.append(PROJ_PATH)
|
|
|
21
21
|
logger=logging.getLogger(__file__)
|
|
22
22
|
class Container:
|
|
23
23
|
def __init__(A,user_id=_C,workspace_path=_B,dockerfile_path='.',image_name=_J,container_basename='sandbox'):A.client=docker.from_env();A.container_basename=container_basename;A.workspace_path=workspace_path;A.dockerfile_path=dockerfile_path;A.image_name=image_name;A.sock=_B;A.queue=_B;A.container=_B;A.user_id=user_id;A.images={_G:'ubuntu:22.04',_H:'python:3.12-slim','node':'node:20-bullseye-slim',_C:_J}
|
|
24
|
-
def
|
|
24
|
+
def bs(A):
|
|
25
25
|
try:
|
|
26
26
|
B=A.client.images.get(A.image_name)
|
|
27
27
|
if B:logger.info(f"Image {A.image_name} exists.");return _A
|
|
28
28
|
else:logger.info(f"Image {A.image_name} not exists.");return _E
|
|
29
29
|
except docker.errors.ImageNotFound:logger.error(f"Image {A.image_name} not exists.");return _E
|
|
30
30
|
except Exception as C:logger.error(f"检查镜像时出错: {str(C)}");return _E
|
|
31
|
-
def
|
|
31
|
+
def bt(B):
|
|
32
32
|
F='arm64';print('🔧 Building Docker image...')
|
|
33
33
|
try:
|
|
34
34
|
C=platform.machine()
|
|
@@ -44,7 +44,7 @@ class Container:
|
|
|
44
44
|
except Exception as D:logger.error(f"An unexpected error occurred: {D}");raise
|
|
45
45
|
def get_or_create_container(A,container_name,image_type=_C):
|
|
46
46
|
E='running';C=image_type;B=container_name;D=_F;F=A.workspace_path;B=f"{A.container_basename}_{A.user_id}"
|
|
47
|
-
if C==_C and not A.
|
|
47
|
+
if C==_C and not A.bs():A.bt()
|
|
48
48
|
try:
|
|
49
49
|
A.container=A.client.containers.get(B)
|
|
50
50
|
if A.container.status==E:logger.info(f"容器 {B} 已存在且运行中,直接复用")
|
|
@@ -19,11 +19,11 @@ logger=logging.getLogger(__name__)
|
|
|
19
19
|
class DynamicContentState(AgentState):review_contents:NotRequired[Annotated[dict[str,str],PrivateStateAttr]];security_contents:NotRequired[Annotated[dict[str,str],PrivateStateAttr]]
|
|
20
20
|
class DynamicContentUpdate(TypedDict):review_contents:dict[str,str];security_contents:dict[str,str]
|
|
21
21
|
REVIEW_SYSTEM_PROMPT=' \n<review_rules>\n {review_rules}\n</review_rules>\n\n<review_guidelines>\n 如果用户要求对项目进行 Review ,请严格依照 review_rules 标签中的规则 对项目进行 Review 。\n</review_guidelines>\n'
|
|
22
|
-
SECURITY_SYSTEM_PROMPT=' \n<security_rules>\n {security_rules}\n</security_rules>\n\n
|
|
22
|
+
SECURITY_SYSTEM_PROMPT=' \n<security_rules>\n {security_rules}\n</security_rules>\n\n<security_guidelines>\n 如果用户要求对项目进行 安全检查 ,请严格依照 security_rules 标签中的规则 对项目进行安全检查 。\n</security_guidelines>\n'
|
|
23
23
|
class DynamicContentMiddleware(AgentMiddleware[DynamicContentState,ContextT,ResponseT]):
|
|
24
24
|
state_schema=DynamicContentState
|
|
25
25
|
def __init__(A,*,home_path=os.path.expanduser('~'),project_path=os.getcwd()):A.home_path=home_path;A.project_path=project_path
|
|
26
|
-
def
|
|
26
|
+
def cv(G,prompt,content_type,contents):
|
|
27
27
|
E='( No rules loaded)';C=contents;B=content_type;A=prompt
|
|
28
28
|
if not C:return A.format(**{B:E})
|
|
29
29
|
D=[f"{A}\n{C[A]}"for A in C]
|
|
@@ -58,7 +58,7 @@ class DynamicContentMiddleware(AgentMiddleware[DynamicContentState,ContextT,Resp
|
|
|
58
58
|
with open(H,_A,encoding=_B)as B:F[H]=B.read()
|
|
59
59
|
return DynamicContentUpdate(review_contents=C,security_contents=F)
|
|
60
60
|
def modify_request(C,request):
|
|
61
|
-
B=request;F=B.state.get(_C,{});G=B.state.get(_D,{});D=C.
|
|
61
|
+
B=request;F=B.state.get(_C,{});G=B.state.get(_D,{});D=C.cv(REVIEW_SYSTEM_PROMPT,'review_rules',F);E=C.cv(SECURITY_SYSTEM_PROMPT,'security_rules',G);A=B.system_message
|
|
62
62
|
if D:A=append_to_system_message(A,D)
|
|
63
63
|
if E:A=append_to_system_message(A,E)
|
|
64
64
|
return B.override(system_message=A)
|
|
@@ -21,12 +21,12 @@ class ReviewConfig(TypedDict):action_name:str;allowed_decisions:list[DecisionTyp
|
|
|
21
21
|
class HITLRequest(TypedDict):action_requests:list[ActionRequest];review_configs:list[ReviewConfig]
|
|
22
22
|
class HumanInTheLoopMiddleware(AgentMiddleware[StateT,ContextT]):
|
|
23
23
|
def __init__(A,interrupt_on=[],*,description_perfix='Tool execution requires approval'):super().__init__();A.interrupt_on=interrupt_on;A.description_prefix=description_perfix
|
|
24
|
-
def
|
|
24
|
+
def ce(D,tool_call):A=tool_call;B=A[_E];C=A[_F];E=A['id'];F=f"{D.description_prefix}\n\nTool: {B}\nArgs: {C}";G=ActionRequest(name=B,id=E,args=C,description=F);H=ReviewConfig(action_name=B,allowed_decisions=[_B,_A,_C]);return G,H
|
|
25
25
|
async def awrap_tool_call(E,request,handler):
|
|
26
26
|
C=handler;A=request;F=A.tool_call['id'];D=A.tool_call[_E];R=A.tool_call[_F];G=A.runtime;H=G.context;I=H.tool_mode
|
|
27
27
|
if I==_A:return await C(A)
|
|
28
28
|
if len(E.interrupt_on)>0 and not D.strip().lower()in E.interrupt_on:return await C(A)
|
|
29
|
-
J,K=E.
|
|
29
|
+
J,K=E.ce(A.tool_call);L=HITLRequest(action_requests=[J],review_configs=[K]);M=interrupt(L);B=M[_G][0][_D]
|
|
30
30
|
if B==_B:return await C(A)
|
|
31
31
|
if B==_A:return await C(A)
|
|
32
32
|
if B==_C:N=f"User rejected the tool call for `{D}` with id {F}";O=ToolMessage(content=N,name=D,tool_call_id=F,status='error');return O
|
|
@@ -35,7 +35,7 @@ class HumanInTheLoopMiddleware(AgentMiddleware[StateT,ContextT]):
|
|
|
35
35
|
C=handler;A=request;F=A.tool_call['id'];D=A.tool_call[_E];R=A.tool_call[_F];H=A.runtime;I=H.context;J=I.tool_mode
|
|
36
36
|
if J==_A:return C(A)
|
|
37
37
|
if len(E.interrupt_on)>0 and not D.strip().lower()in E.interrupt_on:return C(A)
|
|
38
|
-
K,L=E.
|
|
38
|
+
K,L=E.ce(A.tool_call);M=HITLRequest(action_requests=[K],review_configs=[L]);G=interrupt(M);print(f"[HITL] -> Human decision: {G}");B=G[_G][0][_D]
|
|
39
39
|
if B==_B:return C(A)
|
|
40
40
|
if B==_A:return C(A)
|
|
41
41
|
if B==_C:N=f"User rejected the tool call for `{D}` with id {F}";O=ToolMessage(content=N,name=D,tool_call_id=F,status='error');return O
|
|
@@ -14,7 +14,7 @@ MEMORY_SYSTEM_PROMPT=' \n<agent_memory>\n{agent_memory}\n</agent_memory>\n\n<mem
|
|
|
14
14
|
class MemoryMiddleware(AgentMiddleware[MemoryState,ContextT,ResponseT]):
|
|
15
15
|
state_schema=MemoryState
|
|
16
16
|
def __init__(A,*,home_path=os.path.expanduser('~'),project_path=os.getcwd()):A.home_path=home_path;A.project_path=project_path
|
|
17
|
-
def
|
|
17
|
+
def cf(D,contents):
|
|
18
18
|
C='(No memory loaded)';A=contents
|
|
19
19
|
if not A:return MEMORY_SYSTEM_PROMPT.format(agent_memory=C)
|
|
20
20
|
B=[f"{B}\n{A[B]}"for B in D.sources if A.get(B)]
|
|
@@ -39,6 +39,6 @@ class MemoryMiddleware(AgentMiddleware[MemoryState,ContextT,ResponseT]):
|
|
|
39
39
|
H=f"Failed to download {C}: {A.error}";raise ValueError(H)
|
|
40
40
|
if A.content is not None:E[C]=A.content.decode(_B);logger.debug('Loaded memory from: %s',C)
|
|
41
41
|
return MemoryStateUpdate(memory_contents=E)
|
|
42
|
-
def modify_request(B,request):A=request;C=A.state.get(_A,{});D=B.
|
|
42
|
+
def modify_request(B,request):A=request;C=A.state.get(_A,{});D=B.cf(C);E=append_to_system_message(A.system_message,D);return A.override(system_message=E)
|
|
43
43
|
def wrap_model_call(A,request,handler):B=A.modify_request(request);return handler(B)
|
|
44
44
|
async def awrap_model_call(A,request,handler):B=A.modify_request(request);return await handler(B)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from collections.abc import Awaitable,Callable
|
|
2
|
+
from langchain.agents.middleware.types import AgentMiddleware,ModelRequest,ModelResponse
|
|
3
|
+
from langchain.tools import BaseTool,ToolRuntime
|
|
4
|
+
from langchain_core.language_models import BaseChatModel
|
|
5
|
+
from langchain_core.messages import HumanMessage,ToolMessage
|
|
6
|
+
from langchain_core.runnables import Runnable
|
|
7
|
+
from langchain_core.tools import StructuredTool
|
|
8
|
+
from langgraph.types import Command
|
|
9
|
+
from langchain_core.tools import tool
|
|
10
|
+
from src.managers.manager_skill import SkillManager
|
|
11
|
+
def generate_skill_tool(manager):
|
|
12
|
+
B=manager;A=' Execute a skill within the main conversation.\n\nSkills are folders of instructions, scripts, and resources that Claude loads\ndynamically to improve performance on specialized tasks.\n\nUsage:\n- Invoke skills using this tool with the skill name only (no arguments)\n- When you invoke a skill, its prompt will expand and provide detailed instructions\n- Only use skills listed in <available_skills> in the system prompt\n\nImportant:\n- Only use skills that are listed as available\n- Do not use skills that are not listed as available\n'
|
|
13
|
+
@tool(description=A)
|
|
14
|
+
def C(skill):
|
|
15
|
+
D='success';C=skill;A=B.get(C)
|
|
16
|
+
if A is None:E=[A.name for A in B.list_all()];return{D:False,'error':f"Unknown skill: {C}",'available_skills':E[:10],'hint':'Use one of the available skills listed above'}
|
|
17
|
+
F=A.get_prompt();return{D:True,'skill_name':A.name,'skill_description':A.description,'skill_location':A.location,'skill_path':str(A.path),'prompt':F,'message':f'The "{A.name}" skill is loading','allowed_tools':A.allowed_tools}
|
|
18
|
+
return C
|
|
19
|
+
class SkillMiddleware(AgentMiddleware):
|
|
20
|
+
def __init__(A,*,home_path,workspace):B=home_path;super().__init__();A.workspace=workspace;A.home_path=B;A.manager=SkillManager(A.workspace,B);A.manager.load_all();C=generate_skill_tool(A.manager);A.tools=[C]
|
|
21
|
+
def modify_request(D,request):
|
|
22
|
+
A=request;B=D.manager.generate_skills_prompt(5000)
|
|
23
|
+
if A.system_prompt:C=A.system_prompt+'\n\n'+B
|
|
24
|
+
else:C=B
|
|
25
|
+
return A.override(system_prompt=C)
|
|
26
|
+
def wrap_model_call(A,request,handler):B=A.modify_request(request);return handler(B)
|
|
27
|
+
async def awrap_model_call(A,request,handler):B=A.modify_request(request);return await handler(B)
|
|
@@ -79,7 +79,7 @@ async def astream_handler(stream,runtime):
|
|
|
79
79
|
if isinstance(A,AIMessage)and len(A.tool_calls)==0:C=A
|
|
80
80
|
if isinstance(A,ToolMessage):G=A.name;N=A.content;H=A.tool_call_id;I={_E:'tool_result',_G:D.tool_call_id,_H:H,_I:'',_C:G,_D:{},_J:N,_K:_O};J(json.dumps(I,indent=2,ensure_ascii=_L))
|
|
81
81
|
O={_B:[C]if C and isinstance(C,AIMessage)else[]};return O
|
|
82
|
-
def
|
|
82
|
+
def cd(*,default_model,default_tools,default_middleware,default_interrupt_on,subagents,general_purpose_agent,task_description=_A):
|
|
83
83
|
G='Tool call ID is required for subagent invocation';F='custom';A=task_description;B,H=cc(default_model=default_model,default_tools=default_tools,default_middleware=default_middleware,default_interrupt_on=default_interrupt_on,subagents=subagents,general_purpose_agent=general_purpose_agent);C='\n'.join(H)
|
|
84
84
|
def D(result,tool_call_id):A=result;B={A:B for(A,B)in A.items()if A not in _EXCLUDED_STATE_KEYS};C=A[_B][-1];D=I(C);return Command(update={**B,_B:[ToolMessage(D,tool_call_id=tool_call_id)]})
|
|
85
85
|
def I(msg):
|
|
@@ -110,7 +110,7 @@ def bz(*,default_model,default_tools,default_middleware,default_interrupt_on,sub
|
|
|
110
110
|
return D(L,A.tool_call_id)
|
|
111
111
|
return StructuredTool.from_function(name='task',func=J,coroutine=K,description=A)
|
|
112
112
|
class SubAgentMiddleware(AgentMiddleware):
|
|
113
|
-
def __init__(A,*,default_model,default_tools=_A,default_middleware=_A,default_interrupt_on=_A,subagents=_A,system_prompt=TASK_SYSTEM_PROMPT,general_purpose_agent=True,task_description=_A):super().__init__();A.system_prompt=system_prompt;B=
|
|
113
|
+
def __init__(A,*,default_model,default_tools=_A,default_middleware=_A,default_interrupt_on=_A,subagents=_A,system_prompt=TASK_SYSTEM_PROMPT,general_purpose_agent=True,task_description=_A):super().__init__();A.system_prompt=system_prompt;B=cd(default_model=default_model,default_tools=default_tools or[],default_middleware=default_middleware,default_interrupt_on=default_interrupt_on,subagents=subagents or[],general_purpose_agent=general_purpose_agent,task_description=task_description);A.tools=[B]
|
|
114
114
|
def wrap_model_call(B,request,handler):
|
|
115
115
|
C=handler;A=request
|
|
116
116
|
if B.system_prompt is not _A:D=A.system_prompt+'\n\n'+B.system_prompt if A.system_prompt else B.system_prompt;return C(A.override(system_prompt=D))
|
|
@@ -27,7 +27,7 @@ ContextFraction=tuple[Literal[_D],float]
|
|
|
27
27
|
ContextTokens=tuple[Literal[_E],int]
|
|
28
28
|
ContextMessages=tuple[Literal[_B],int]
|
|
29
29
|
ContextSize=ContextFraction|ContextTokens|ContextMessages
|
|
30
|
-
def
|
|
30
|
+
def cm(model):
|
|
31
31
|
if model._llm_type=='anthropic-chat':return partial(count_tokens_approximately,chars_per_token=3.3)
|
|
32
32
|
return count_tokens_approximately
|
|
33
33
|
class SummaryState(AgentState):compact:NotRequired[bool]=_F
|
|
@@ -44,61 +44,61 @@ class SummarizationMiddleware(AgentMiddleware):
|
|
|
44
44
|
if isinstance(C,str):C=init_chat_model(C)
|
|
45
45
|
A.model=C
|
|
46
46
|
if B is _A:A.trigger=_A;G=[]
|
|
47
|
-
elif isinstance(B,list):I=[A.
|
|
48
|
-
else:J=A.
|
|
49
|
-
A._trigger_conditions=G;A.keep=A.
|
|
50
|
-
if H is count_tokens_approximately:A.token_counter=
|
|
47
|
+
elif isinstance(B,list):I=[A.cq(B,N)for B in B];A.trigger=I;G=I
|
|
48
|
+
else:J=A.cq(B,N);A.trigger=J;G=[J]
|
|
49
|
+
A._trigger_conditions=G;A.keep=A.cq(F,'keep')
|
|
50
|
+
if H is count_tokens_approximately:A.token_counter=cm(A.model)
|
|
51
51
|
else:A.token_counter=H
|
|
52
52
|
A.summary_prompt=summary_prompt;A.trim_tokens_to_summarize=trim_tokens_to_summarize;K=any(A[0]==_D for A in A._trigger_conditions)
|
|
53
53
|
if A.keep[0]==_D:K=_C
|
|
54
|
-
if K and A.
|
|
55
|
-
def
|
|
54
|
+
if K and A.cj()is _A:O='Model profile information is required to use fractional token limits, and is unavailable for the specified model. Please use absolute token counts instead, or pass `\n\nChatModel(..., profile={"max_input_tokens": ...})`.\n\nwith a desired integer value of the model\'s maximum input tokens.';raise ValueError(O)
|
|
55
|
+
def ct(B,state):A=state.get(_G,_F);return bool(A)
|
|
56
56
|
@override
|
|
57
57
|
def before_model(self,state,runtime):
|
|
58
|
-
C=state;A=self;B=C[_B];A.
|
|
58
|
+
C=state;A=self;B=C[_B];A.co(B);E=A.token_counter(B);F=A.ct(C);G=A.ch(B,E)
|
|
59
59
|
if not G and not F:return
|
|
60
|
-
D=A.
|
|
60
|
+
D=A.cu(B)
|
|
61
61
|
if D<=0:return
|
|
62
|
-
H,I=A.
|
|
62
|
+
H,I=A.cn(B,D);J=A.ck(H);K=A.cl(J);return{_B:[RemoveMessage(id=REMOVE_ALL_MESSAGES),*K,*I],_G:_F}
|
|
63
63
|
@override
|
|
64
64
|
async def abefore_model(self,state,runtime):
|
|
65
|
-
C=state;A=self;B=C[_B];A.
|
|
65
|
+
C=state;A=self;B=C[_B];A.co(B);E=A.token_counter(B);F=A.ct(C);G=A.ch(B,E)
|
|
66
66
|
if not G and not F:return
|
|
67
|
-
D=A.
|
|
67
|
+
D=A.cu(B)
|
|
68
68
|
if D<=0:return
|
|
69
|
-
H,I=A.
|
|
70
|
-
def
|
|
69
|
+
H,I=A.cn(B,D);J=await A._acreate_summary(H);K=A.cl(J);return{_B:[RemoveMessage(id=REMOVE_ALL_MESSAGES),*K,*I],_G:_F}
|
|
70
|
+
def ci(B,messages,threshold):
|
|
71
71
|
A=next((A for A in reversed(messages)if isinstance(A,AIMessage)),_A)
|
|
72
72
|
if isinstance(A,AIMessage)and A.usage_metadata is not _A and(C:=A.usage_metadata.get('total_tokens',-1))and C>=threshold and(D:=A.response_metadata.get('model_provider'))and D==B.model._get_ls_params().get('ls_provider'):return _C
|
|
73
73
|
return _F
|
|
74
|
-
def
|
|
74
|
+
def ch(A,messages,total_tokens):
|
|
75
75
|
F=total_tokens;E=messages
|
|
76
76
|
if not A._trigger_conditions:return _F
|
|
77
77
|
for(B,C)in A._trigger_conditions:
|
|
78
78
|
if B==_B and len(E)>=C:return _C
|
|
79
79
|
if B==_E and F>=C:return _C
|
|
80
|
-
if B==_E and A.
|
|
80
|
+
if B==_E and A.ci(E,C):return _C
|
|
81
81
|
if B==_D:
|
|
82
|
-
G=A.
|
|
82
|
+
G=A.cj()
|
|
83
83
|
if G is _A:continue
|
|
84
84
|
D=int(G*C)
|
|
85
85
|
if D<=0:D=1
|
|
86
86
|
if F>=D:return _C
|
|
87
|
-
if A.
|
|
87
|
+
if A.ci(E,D):return _C
|
|
88
88
|
return _F
|
|
89
|
-
def
|
|
89
|
+
def cu(A,messages):
|
|
90
90
|
B=messages;D,E=A.keep
|
|
91
91
|
if D in{_E,_D}:
|
|
92
|
-
C=A.
|
|
92
|
+
C=A.cs(B)
|
|
93
93
|
if C is not _A:return C
|
|
94
|
-
return A.
|
|
95
|
-
return A.
|
|
96
|
-
def
|
|
94
|
+
return A.cp(B,_DEFAULT_MESSAGES_TO_KEEP)
|
|
95
|
+
return A.cp(B,cast('int',E))
|
|
96
|
+
def cs(C,messages):
|
|
97
97
|
A=messages
|
|
98
98
|
if not A:return 0
|
|
99
99
|
H,I=C.keep
|
|
100
100
|
if H==_D:
|
|
101
|
-
J=C.
|
|
101
|
+
J=C.cj()
|
|
102
102
|
if J is _A:return
|
|
103
103
|
F=int(J*I)
|
|
104
104
|
elif H==_E:F=int(I)
|
|
@@ -114,8 +114,8 @@ class SummarizationMiddleware(AgentMiddleware):
|
|
|
114
114
|
if B>=len(A):
|
|
115
115
|
if len(A)==1:return 0
|
|
116
116
|
B=len(A)-1
|
|
117
|
-
return C.
|
|
118
|
-
def
|
|
117
|
+
return C.cr(A,B)
|
|
118
|
+
def cj(C):
|
|
119
119
|
try:A=C.model.profile
|
|
120
120
|
except AttributeError:return
|
|
121
121
|
if not isinstance(A,Mapping):return
|
|
@@ -123,7 +123,7 @@ class SummarizationMiddleware(AgentMiddleware):
|
|
|
123
123
|
if not isinstance(B,int):return
|
|
124
124
|
return B
|
|
125
125
|
@staticmethod
|
|
126
|
-
def
|
|
126
|
+
def cq(context,parameter_name):
|
|
127
127
|
E=context;C=parameter_name;D,B=E
|
|
128
128
|
if D==_D:
|
|
129
129
|
if not 0<B<=1:A=f"Fractional {C} values must be between 0 and 1, got {B}.";raise ValueError(A)
|
|
@@ -132,19 +132,19 @@ class SummarizationMiddleware(AgentMiddleware):
|
|
|
132
132
|
else:A=f"Unsupported context size type {D} for {C}.";raise ValueError(A)
|
|
133
133
|
return E
|
|
134
134
|
@staticmethod
|
|
135
|
-
def
|
|
135
|
+
def cl(summary):return[HumanMessage(content=f"Here is a summary of the conversation to date:\n\n{summary}",additional_kwargs={'lc_source':'summarization'})]
|
|
136
136
|
@staticmethod
|
|
137
|
-
def
|
|
137
|
+
def co(messages):
|
|
138
138
|
for A in messages:
|
|
139
139
|
if A.id is _A:A.id=str(uuid.uuid4())
|
|
140
140
|
@staticmethod
|
|
141
|
-
def
|
|
142
|
-
def
|
|
141
|
+
def cn(conversation_messages,cutoff_index):B=cutoff_index;A=conversation_messages;C=A[:B];D=A[B:];return C,D
|
|
142
|
+
def cp(C,messages,messages_to_keep):
|
|
143
143
|
B=messages_to_keep;A=messages
|
|
144
144
|
if len(A)<=B:return 0
|
|
145
|
-
D=len(A)-B;return C.
|
|
145
|
+
D=len(A)-B;return C.cr(A,D)
|
|
146
146
|
@staticmethod
|
|
147
|
-
def
|
|
147
|
+
def cr(messages,cutoff_index):
|
|
148
148
|
B=cutoff_index;A=messages
|
|
149
149
|
if B>=len(A)or not isinstance(A[B],ToolMessage):return B
|
|
150
150
|
E=set();C=B
|
|
@@ -158,10 +158,10 @@ class SummarizationMiddleware(AgentMiddleware):
|
|
|
158
158
|
H={A.get('id')for A in D.tool_calls if A.get('id')}
|
|
159
159
|
if E&H:return G
|
|
160
160
|
return C
|
|
161
|
-
def
|
|
161
|
+
def ck(A,messages_to_sumarize):
|
|
162
162
|
B=messages_to_sumarize
|
|
163
163
|
if not B:return _H
|
|
164
|
-
C=A.
|
|
164
|
+
C=A.cg(B)
|
|
165
165
|
if not C:return'Previous conversation was too long to summarize'
|
|
166
166
|
D=get_buffer_string(C)
|
|
167
167
|
try:E=A.model.invoke(A.summary_prompt.format(messages=D));return E.text.strip()
|
|
@@ -169,12 +169,12 @@ class SummarizationMiddleware(AgentMiddleware):
|
|
|
169
169
|
async def _acreate_summary(A,messages_to_summarize):
|
|
170
170
|
B=messages_to_summarize
|
|
171
171
|
if not B:return _H
|
|
172
|
-
C=A.
|
|
172
|
+
C=A.cg(B)
|
|
173
173
|
if not C:return'Previous conversation was too long to summarize.'
|
|
174
174
|
D=get_buffer_string(C)
|
|
175
175
|
try:E=await A.model.ainvoke(A.summary_prompt.format(messages=D));return E.text.strip()
|
|
176
176
|
except Exception as F:return f"Error generating sumamry: {F!s}"
|
|
177
|
-
def
|
|
177
|
+
def cg(A,messages):
|
|
178
178
|
B=messages
|
|
179
179
|
try:
|
|
180
180
|
if A.trim_tokens_to_summarize is _A:return B
|
|
@@ -16,33 +16,33 @@ class ToolResultFormatter:
|
|
|
16
16
|
def detect_type(B,content):
|
|
17
17
|
A=content;A=A.strip()
|
|
18
18
|
if A.statswith(SUCCESS_PREFIX):
|
|
19
|
-
C=B.
|
|
20
|
-
if B.
|
|
19
|
+
C=B.bh(A)
|
|
20
|
+
if B.bi(C):return ContentType.JSON
|
|
21
21
|
return ContentType.SUCCESS
|
|
22
22
|
if A.startswith(FAILURE_PREFIX):return ContentType.ERROR
|
|
23
|
-
if B.
|
|
24
|
-
if B.
|
|
25
|
-
if B.
|
|
23
|
+
if B.bi(A):return ContentType.JSON
|
|
24
|
+
if B.bd(A):return ContentType.ERROR
|
|
25
|
+
if B.bg(A):return ContentType.MARKDOWN
|
|
26
26
|
return ContentType.TEXT
|
|
27
27
|
def is_success(A,content):return _is_success(content)
|
|
28
|
-
def format(A,name,content,max_length=800):B=content;C=A.detect_type(B);D=A.is_success(B);E={ContentType.SUCCESS:A.
|
|
29
|
-
def
|
|
30
|
-
def
|
|
28
|
+
def format(A,name,content,max_length=800):B=content;C=A.detect_type(B);D=A.is_success(B);E={ContentType.SUCCESS:A.be,ContentType.ERROR:A.bc,ContentType.JSON:A.ba,ContentType.MARKDOWN:A.bj,ContentType.TEXT:A.bb};F=E.get(C,A.bb);G=F(name,B,max_length);return FormattedResult(content_type=C,elements=G,success=D)
|
|
29
|
+
def bh(B,content):A=content.split('\n',2);return A[2].strip()if len(A)>2 else''
|
|
30
|
+
def bi(B,content):
|
|
31
31
|
A=content;A=A.strip()
|
|
32
32
|
if not A:return _A
|
|
33
33
|
if A.startswith('{')and A.endswith('}')or A.startswith('[')and A.endswith(']'):
|
|
34
34
|
try:json.loads(A);return True
|
|
35
35
|
except(json.JSONDecodeError,ValueError):pass
|
|
36
36
|
return _A
|
|
37
|
-
def
|
|
38
|
-
def
|
|
39
|
-
def
|
|
40
|
-
def
|
|
41
|
-
def
|
|
37
|
+
def bd(B,content):A=['Traceback (most recent call last)','Exception:','Error:'];return any(A in content for A in A)
|
|
38
|
+
def bg(C,content):A=content;B=['```','**','##','- **'];return A.startswith('#')or any(B in A for B in B)
|
|
39
|
+
def be(B,name,content,max_length):A='green';C=B.bf(content,max_length);return[Panel(Text(C,style=A),title=f"📤 {name} ✓",border_style=A)]
|
|
40
|
+
def bc(B,name,content,max_length):A='red';C=B.bf(content,max_length);return[Panel(Text(C,style=A),title=f"📤 {name} ✗",border_style=A)]
|
|
41
|
+
def ba(B,name,content,max_length):
|
|
42
42
|
D=max_length;A=content;E=A
|
|
43
|
-
if A.startswith(SUCCESS_PREFIX):E=B.
|
|
44
|
-
try:F=json.loads(E);C=json.dumps(F,indent=2,ensure_ascii=_A);C=B.
|
|
45
|
-
except(json.JSONDecodeError,ValueError):return B.
|
|
46
|
-
def
|
|
47
|
-
def
|
|
48
|
-
def
|
|
43
|
+
if A.startswith(SUCCESS_PREFIX):E=B.bh(A)
|
|
44
|
+
try:F=json.loads(E);C=json.dumps(F,indent=2,ensure_ascii=_A);C=B.bf(C,D);return[Text(f"📤 {name} ✓",style=_B),Syntax(C,'json',theme='monokai',line_numbers=_A)]
|
|
45
|
+
except(json.JSONDecodeError,ValueError):return B.bb(name,A,D)
|
|
46
|
+
def bj(A,name,content,max_length):B=A.bf(content,max_length);return[Panel(Markdown(B),title=f"📤 {name}",border_style='cyan dim')]
|
|
47
|
+
def bb(A,name,content,max_length):B=A.bf(content,max_length);return[Text(f"📤 {name}:",style=_B),Text(f" {B}",style='dim')]
|
|
48
|
+
def bf(A,content,max_length):return truncate(content,max_length)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
_A=None
|
|
3
|
+
import argparse,json,sys
|
|
4
|
+
from datetime import datetime,timedelta,timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from.pricing import ModelPricing
|
|
7
|
+
from.tracker import CostTracker,UsageRecord,BudgetGuard
|
|
8
|
+
from.report import CostReport
|
|
9
|
+
def parse_since(s):
|
|
10
|
+
A=datetime.now(timezone.utc);s=s.strip().lower()
|
|
11
|
+
if s.endswith('d'):return A-timedelta(days=int(s[:-1]))
|
|
12
|
+
if s.endswith('w'):return A-timedelta(weeks=int(s[:-1]))
|
|
13
|
+
if s.endswith('m'):return A-timedelta(days=30*int(s[:-1]))
|
|
14
|
+
if s.endswith('h'):return A-timedelta(hours=int(s[:-1]))
|
|
15
|
+
return datetime.fromisoformat(s)
|
|
16
|
+
def cmd_add(args,tracker):
|
|
17
|
+
B=tracker;A=args;D=B.add_call(model=A.model,input_tokens=A.input,output_tokens=A.output,tag=A.tag);E=ModelPricing()
|
|
18
|
+
try:F=E.comp(A.model,A.input,A.output);C=f"${F:.6f}"
|
|
19
|
+
except KeyError:C='unknown (model not in pricing DB)'
|
|
20
|
+
print(f"✓ Logged: {A.model} {A.input:,} in / {A.output:,} out cost={C} tag={A.tag}");print(f" record_id={D.record_id} db={B.db_path}")
|
|
21
|
+
def cmd_report(args,tracker):
|
|
22
|
+
A=args;B=parse_since(A.since)if A.since else _A;C=CostReport(tracker)
|
|
23
|
+
if A.format=='json':print(C.to_json(since=B,period=A.period))
|
|
24
|
+
else:print(C.to_text(since=B,period=A.period))
|
|
25
|
+
def cmd_models(args):
|
|
26
|
+
C=ModelPricing();B=C.list_models(provider=args.provider)
|
|
27
|
+
if not B:print('No models found.');return
|
|
28
|
+
print(f"\n{'Model':<45} {'Provider':<12} {'Input $/1M':>12} {'Output $/1M':>12}");print('─'*85)
|
|
29
|
+
for A in B:print(f" {A.model:<43} {A.provider:<12} ${A.input_per_1m:>10.3f} ${A.output_per_1m:>10.3f}")
|
|
30
|
+
print()
|
|
31
|
+
def cmd_budget(args,tracker):
|
|
32
|
+
C=tracker;A=args
|
|
33
|
+
if A.budget_cmd=='set':D=BudgetGuard(tag=A.tag,limit_usd=A.limit);C.set_budget(D);print(f"✓ Budget set: tag={A.tag} limit=${A.limit:.2f}");B=C.total_cost(tag=A.tag);E=B/A.limit*100 if A.limit>0 else 0;print(f" Current spend: ${B:.4f} ({E:.1f}% of budget)")
|
|
34
|
+
elif A.budget_cmd=='check':B=C.total_cost(tag=A.tag);print(f" Tag '{A.tag}': ${B:.4f} spent")
|
|
35
|
+
else:print('Usage: llm-cost budget set --tag TAG --limit AMOUNT')
|
|
36
|
+
def main():
|
|
37
|
+
P='budget';O='models';N='daily';M='text';L='report';K='add';H='--tag';B=True;C=argparse.ArgumentParser(prog='llm-cost',description='LLM cost tracker — log API calls, report spending, guard budgets.');C.add_argument('--db',type=Path,default=_A,help='Path to usage DB (default: ~/.config/llm-cost-tracker/usage.jsonl)');E=C.add_subparsers(dest='command');D=E.add_parser(K,help='Log an LLM API call');D.add_argument('--model',required=B,help='Model name, e.g. gpt-4o');D.add_argument('--input',type=int,required=B,help='Input token count');D.add_argument('--output',type=int,required=B,help='Output token count');D.add_argument(H,default='default',help='Project/tag label');D.add_argument('--timestamp',default=_A,help='ISO timestamp (default: now)');F=E.add_parser(L,help='Show cost report');F.add_argument('--since',default='30d',help='e.g. 7d, 1w, 1m, or ISO date');F.add_argument('--format',choices=[M,'json'],default=M);F.add_argument('--period',choices=[N,'weekly','monthly'],default=N);Q=E.add_parser(O,help='List pricing database');Q.add_argument('--provider',default=_A,help='Filter by provider');R=E.add_parser(P,help='Budget guard commands');I=R.add_subparsers(dest='budget_cmd');J=I.add_parser('set',help='Set a budget for a tag');J.add_argument(H,required=B);J.add_argument('--limit',type=float,required=B,help='Budget in USD');S=I.add_parser('check',help='Check spending for a tag');S.add_argument(H,required=B);A=C.parse_args()
|
|
38
|
+
if not A.command:C.print_help();sys.exit(0)
|
|
39
|
+
G=CostTracker(db_path=A.db)
|
|
40
|
+
if A.command==K:cmd_add(A,G)
|
|
41
|
+
elif A.command==L:cmd_report(A,G)
|
|
42
|
+
elif A.command==O:cmd_models(A)
|
|
43
|
+
elif A.command==P:cmd_budget(A,G)
|
|
44
|
+
else:C.print_help()
|
|
45
|
+
if __name__=='__main__':main()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
_I='qwen'
|
|
2
|
+
_H='mistral'
|
|
3
|
+
_G='avg hosted price'
|
|
4
|
+
_F='meta'
|
|
5
|
+
_E='cohere'
|
|
6
|
+
_D=None
|
|
7
|
+
_C='google'
|
|
8
|
+
_B='anthropic'
|
|
9
|
+
_A='openai'
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Optional
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ModelRate:provider:str;model:str;input_per_1m:float;output_per_1m:float;notes:str=''
|
|
14
|
+
_PRICING={}
|
|
15
|
+
def cy(*C):
|
|
16
|
+
for A in C:
|
|
17
|
+
_PRICING[A.model]=A;B=A.model.split('/')[-1]if'/'in A.model else _D
|
|
18
|
+
if B and B not in _PRICING:_PRICING[B]=A
|
|
19
|
+
cy(ModelRate(_A,'gpt-4o',2.5,1e1),ModelRate(_A,'gpt-4o-mini',.15,.6),ModelRate(_A,'gpt-4o-2024-11-20',2.5,1e1),ModelRate(_A,'gpt-4-turbo',1e1,3e1),ModelRate(_A,'gpt-4',3e1,6e1),ModelRate(_A,'gpt-3.5-turbo',.5,1.5),ModelRate(_A,'o1',15.,6e1),ModelRate(_A,'o1-mini',3.,12.),ModelRate(_A,'o3-mini',1.1,4.4),ModelRate(_A,'o3',1e1,4e1),ModelRate(_B,'claude-3-5-sonnet-20241022',3.,15.),ModelRate(_B,'claude-3-5-haiku-20241022',.8,4.),ModelRate(_B,'claude-3-opus-20240229',15.,75.),ModelRate(_B,'claude-3-sonnet-20240229',3.,15.),ModelRate(_B,'claude-3-haiku-20240307',.25,1.25),ModelRate(_B,'claude-sonnet-4-5',3.,15.),ModelRate(_B,'claude-sonnet-4-6',3.,15.),ModelRate(_B,'claude-opus-4',15.,75.),ModelRate(_C,'gemini-1.5-pro',3.5,10.5,'up to 128k ctx'),ModelRate(_C,'gemini-1.5-flash',.075,.3),ModelRate(_C,'gemini-2.0-flash',.1,.4),ModelRate(_C,'gemini-2.0-flash-lite',.075,.3),ModelRate(_C,'gemini-2.0-pro',3.5,10.5),ModelRate(_E,'command-r-plus',2.5,1e1),ModelRate(_E,'command-r',.15,.6),ModelRate(_E,'command-r7b',.0375,.15),ModelRate(_F,'llama-3.3-70b-instruct',.23,.4,_G),ModelRate(_F,'llama-3.1-405b-instruct',3.,3.,_G),ModelRate(_F,'llama-3.1-70b-instruct',.2,.2,_G),ModelRate(_H,'mistral-large-2411',2.,6.),ModelRate(_H,'mistral-small-2501',.1,.3),ModelRate(_H,'codestral-2501',.1,.3),ModelRate(_I,'qwen-plus',2.,6.),ModelRate(_I,'qwen-max-3',2.,6.),ModelRate(_I,'qwen-max-3.5',2.,6.))
|
|
20
|
+
class ModelPricing:
|
|
21
|
+
def __init__(A,custom_rates=_D):
|
|
22
|
+
B=custom_rates;A._db=dict(_PRICING)
|
|
23
|
+
if B:A._db.update(B)
|
|
24
|
+
def get_rate(A,model):
|
|
25
|
+
C=model;B=C.lower().strip()
|
|
26
|
+
if B in A._db:return A._db[B]
|
|
27
|
+
for(D,E)in A._db.items():
|
|
28
|
+
if B in D or D in B:return E
|
|
29
|
+
raise KeyError(f"Unknown model: {C!r}. Known models: {sorted(A._db)}")
|
|
30
|
+
def compute_cost(B,model,input_tokens,output_tokens):A=B.get_rate(model);return A.input_per_1m*input_tokens/1000000+A.output_per_1m*output_tokens/1000000
|
|
31
|
+
def list_models(F,provider=_D):
|
|
32
|
+
B=provider;C=set();D=[]
|
|
33
|
+
for A in F._db.values():
|
|
34
|
+
E=A.provider,A.model
|
|
35
|
+
if E in C:continue
|
|
36
|
+
C.add(E)
|
|
37
|
+
if B is _D or A.provider.lower()==B.lower():D.append(A)
|
|
38
|
+
return sorted(D,key=lambda r:(r.provider,r.model))
|
|
39
|
+
def add_custom(A,rate):A._db[rate.model]=rate
|