ctxoptimizer 0.4.1__py3-none-any.whl
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.
- ctxoptimizer/__init__.py +5 -0
- ctxoptimizer/__main__.py +14 -0
- ctxoptimizer/admin_security.py +130 -0
- ctxoptimizer/analysis/__init__.py +1 -0
- ctxoptimizer/analysis/baseline.py +69 -0
- ctxoptimizer/analysis/break_even.py +49 -0
- ctxoptimizer/analysis/drift.py +151 -0
- ctxoptimizer/analysis/edits.py +148 -0
- ctxoptimizer/analysis/markers.py +34 -0
- ctxoptimizer/analysis/recommend.py +114 -0
- ctxoptimizer/analysis/segments.py +57 -0
- ctxoptimizer/analysis/session.py +62 -0
- ctxoptimizer/analysis/state.py +239 -0
- ctxoptimizer/analysis/tokens.py +62 -0
- ctxoptimizer/analysis/waste.py +322 -0
- ctxoptimizer/cli.py +873 -0
- ctxoptimizer/config.py +512 -0
- ctxoptimizer/dashboard/__init__.py +1 -0
- ctxoptimizer/dashboard/api.py +537 -0
- ctxoptimizer/dashboard/service.py +157 -0
- ctxoptimizer/dashboard/static/.gitkeep +1 -0
- ctxoptimizer/dashboard/templates/.gitkeep +1 -0
- ctxoptimizer/dashboard/templates/doctor.html +46 -0
- ctxoptimizer/dashboard/templates/live.html +91 -0
- ctxoptimizer/dashboard/templates/overview.html +172 -0
- ctxoptimizer/dashboard/templates/pricing.html +115 -0
- ctxoptimizer/dashboard/templates/request.html +111 -0
- ctxoptimizer/dashboard/templates/session.html +67 -0
- ctxoptimizer/dashboard/templates/settings.html +79 -0
- ctxoptimizer/dashboard/templates/table.html +56 -0
- ctxoptimizer/dashboard/templates/transforms.html +118 -0
- ctxoptimizer/dashboard/templates/waste.html +120 -0
- ctxoptimizer/dashboard/views.py +429 -0
- ctxoptimizer/data/cache_limits.json +23 -0
- ctxoptimizer/data/default_config.toml +185 -0
- ctxoptimizer/data/pricing.json +421 -0
- ctxoptimizer/doctor.py +129 -0
- ctxoptimizer/guard.py +139 -0
- ctxoptimizer/ids.py +73 -0
- ctxoptimizer/log.py +73 -0
- ctxoptimizer/mcp.py +124 -0
- ctxoptimizer/models.py +145 -0
- ctxoptimizer/pipeline.py +126 -0
- ctxoptimizer/profiles.py +152 -0
- ctxoptimizer/providers/__init__.py +37 -0
- ctxoptimizer/providers/anthropic.py +105 -0
- ctxoptimizer/providers/base.py +191 -0
- ctxoptimizer/providers/gemini.py +143 -0
- ctxoptimizer/providers/ollama.py +133 -0
- ctxoptimizer/providers/openai_chat.py +135 -0
- ctxoptimizer/providers/openai_responses.py +121 -0
- ctxoptimizer/proxy.py +665 -0
- ctxoptimizer/routing.py +259 -0
- ctxoptimizer/server.py +319 -0
- ctxoptimizer/service.py +150 -0
- ctxoptimizer/telemetry/__init__.py +1 -0
- ctxoptimizer/telemetry/blobs.py +57 -0
- ctxoptimizer/telemetry/expand.py +62 -0
- ctxoptimizer/telemetry/pricing.py +168 -0
- ctxoptimizer/telemetry/redact.py +53 -0
- ctxoptimizer/telemetry/sse.py +145 -0
- ctxoptimizer/telemetry/store.py +829 -0
- ctxoptimizer/telemetry/usage.py +435 -0
- ctxoptimizer/transforms/__init__.py +87 -0
- ctxoptimizer/transforms/ansi.py +21 -0
- ctxoptimizer/transforms/anthropic_cache.py +100 -0
- ctxoptimizer/transforms/backref.py +74 -0
- ctxoptimizer/transforms/base.py +93 -0
- ctxoptimizer/transforms/cache_serve.py +146 -0
- ctxoptimizer/transforms/dedupe_lines.py +69 -0
- ctxoptimizer/transforms/eligibility.py +24 -0
- ctxoptimizer/transforms/openai_hints.py +84 -0
- ctxoptimizer/transforms/progress.py +52 -0
- ctxoptimizer/transforms/stale_result_elision.py +109 -0
- ctxoptimizer/transforms/truncate.py +100 -0
- ctxoptimizer/transforms/volatile.py +177 -0
- ctxoptimizer/transforms/whitespace.py +25 -0
- ctxoptimizer-0.4.1.dist-info/METADATA +196 -0
- ctxoptimizer-0.4.1.dist-info/RECORD +83 -0
- ctxoptimizer-0.4.1.dist-info/WHEEL +4 -0
- ctxoptimizer-0.4.1.dist-info/entry_points.txt +3 -0
- ctxoptimizer-0.4.1.dist-info/licenses/LICENSE +21 -0
- ctxoptimizer-0.4.1.dist-info/licenses/NOTICE +13 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Provider adapter factory (SPEC section 11)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ctxoptimizer.models import Endpoint, Provider
|
|
6
|
+
from ctxoptimizer.providers.anthropic import AnthropicAdapter
|
|
7
|
+
from ctxoptimizer.providers.base import PassthroughAdapter, ProviderAdapter
|
|
8
|
+
from ctxoptimizer.providers.gemini import GeminiAdapter
|
|
9
|
+
from ctxoptimizer.providers.ollama import OllamaAdapter
|
|
10
|
+
from ctxoptimizer.providers.openai_chat import OpenAIChatAdapter
|
|
11
|
+
from ctxoptimizer.providers.openai_responses import OpenAIResponsesAdapter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def adapter_for(provider: Provider, endpoint: Endpoint) -> ProviderAdapter:
|
|
15
|
+
if provider is Provider.ANTHROPIC:
|
|
16
|
+
return AnthropicAdapter()
|
|
17
|
+
if provider is Provider.OPENAI:
|
|
18
|
+
if endpoint is Endpoint.RESPONSES:
|
|
19
|
+
return OpenAIResponsesAdapter()
|
|
20
|
+
return OpenAIChatAdapter()
|
|
21
|
+
if provider is Provider.GEMINI:
|
|
22
|
+
return GeminiAdapter()
|
|
23
|
+
if provider is Provider.OLLAMA:
|
|
24
|
+
return OllamaAdapter()
|
|
25
|
+
return PassthroughAdapter(provider)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"AnthropicAdapter",
|
|
30
|
+
"GeminiAdapter",
|
|
31
|
+
"OllamaAdapter",
|
|
32
|
+
"OpenAIChatAdapter",
|
|
33
|
+
"OpenAIResponsesAdapter",
|
|
34
|
+
"PassthroughAdapter",
|
|
35
|
+
"ProviderAdapter",
|
|
36
|
+
"adapter_for",
|
|
37
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Anthropic Messages adapter (SPEC section 11.1)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ctxoptimizer.analysis.segments import Segment, make_segment
|
|
9
|
+
from ctxoptimizer.models import Provider, ToolResultRef
|
|
10
|
+
from ctxoptimizer.providers.base import (
|
|
11
|
+
DictToolResultRef,
|
|
12
|
+
blocks_of,
|
|
13
|
+
text_of,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AnthropicAdapter:
|
|
18
|
+
provider = Provider.ANTHROPIC
|
|
19
|
+
|
|
20
|
+
def model_of(self, payload: dict[str, Any]) -> str | None:
|
|
21
|
+
model = payload.get("model")
|
|
22
|
+
return model if isinstance(model, str) else None
|
|
23
|
+
|
|
24
|
+
def is_stream(self, payload: dict[str, Any]) -> bool:
|
|
25
|
+
return bool(payload.get("stream"))
|
|
26
|
+
|
|
27
|
+
def is_transformable(self, payload: dict[str, Any]) -> bool:
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
def iter_tool_results(self, payload: dict[str, Any]) -> Iterator[ToolResultRef]:
|
|
31
|
+
names = self._tool_use_names(payload)
|
|
32
|
+
for message_index, message in enumerate(payload.get("messages") or []):
|
|
33
|
+
if not isinstance(message, dict):
|
|
34
|
+
continue
|
|
35
|
+
for block_index, block in enumerate(blocks_of(message.get("content"))):
|
|
36
|
+
if block.get("type") != "tool_result":
|
|
37
|
+
continue
|
|
38
|
+
tool_use_id = str(block.get("tool_use_id") or "")
|
|
39
|
+
yield DictToolResultRef(
|
|
40
|
+
block,
|
|
41
|
+
message_index=message_index,
|
|
42
|
+
block_index=block_index,
|
|
43
|
+
tool_use_id=tool_use_id,
|
|
44
|
+
tool_name=names.get(tool_use_id),
|
|
45
|
+
is_error=bool(block.get("is_error")),
|
|
46
|
+
key="content",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def segments(self, payload: dict[str, Any]) -> list[Segment]:
|
|
50
|
+
result: list[Segment] = []
|
|
51
|
+
for index, tool in enumerate(payload.get("tools") or []):
|
|
52
|
+
if isinstance(tool, dict):
|
|
53
|
+
result.append(make_segment("tool_def", tool, block_index=index))
|
|
54
|
+
system = payload.get("system")
|
|
55
|
+
system_blocks = blocks_of(system)
|
|
56
|
+
if isinstance(system, str):
|
|
57
|
+
system_blocks = [{"type": "text", "text": system}]
|
|
58
|
+
for index, block in enumerate(system_blocks):
|
|
59
|
+
result.append(make_segment("system", block, block_index=index))
|
|
60
|
+
for message_index, message in enumerate(payload.get("messages") or []):
|
|
61
|
+
if not isinstance(message, dict):
|
|
62
|
+
continue
|
|
63
|
+
role = str(message.get("role") or "")
|
|
64
|
+
for block_index, block in enumerate(blocks_of(message.get("content"))):
|
|
65
|
+
result.append(
|
|
66
|
+
make_segment(
|
|
67
|
+
"message",
|
|
68
|
+
{**block, "role": role},
|
|
69
|
+
msg_index=message_index,
|
|
70
|
+
block_index=block_index,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
def system_text(self, payload: dict[str, Any]) -> str:
|
|
76
|
+
return text_of(payload.get("system"))
|
|
77
|
+
|
|
78
|
+
def first_user_text(self, payload: dict[str, Any]) -> str:
|
|
79
|
+
for message in payload.get("messages") or []:
|
|
80
|
+
if isinstance(message, dict) and message.get("role") == "user":
|
|
81
|
+
return text_of(message.get("content"))
|
|
82
|
+
return ""
|
|
83
|
+
|
|
84
|
+
def tool_names(self, payload: dict[str, Any]) -> list[str]:
|
|
85
|
+
return [
|
|
86
|
+
str(tool["name"])
|
|
87
|
+
for tool in payload.get("tools") or []
|
|
88
|
+
if isinstance(tool, dict) and isinstance(tool.get("name"), str)
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
def used_tool_names(self, payload: dict[str, Any]) -> list[str]:
|
|
92
|
+
return list(self._tool_use_names(payload).values())
|
|
93
|
+
|
|
94
|
+
def _tool_use_names(self, payload: dict[str, Any]) -> dict[str, str]:
|
|
95
|
+
names: dict[str, str] = {}
|
|
96
|
+
for message in payload.get("messages") or []:
|
|
97
|
+
if not isinstance(message, dict):
|
|
98
|
+
continue
|
|
99
|
+
for block in blocks_of(message.get("content")):
|
|
100
|
+
if block.get("type") == "tool_use" and isinstance(block.get("id"), str):
|
|
101
|
+
names[block["id"]] = str(block.get("name") or "")
|
|
102
|
+
return names
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = ["AnthropicAdapter"]
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Provider adapter protocol and shared payload helpers (SPEC section 11)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from typing import Any, Protocol
|
|
7
|
+
|
|
8
|
+
from ctxoptimizer.analysis.segments import Segment
|
|
9
|
+
from ctxoptimizer.models import Provider, ToolResultRef
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ProviderAdapter(Protocol):
|
|
13
|
+
provider: Provider
|
|
14
|
+
|
|
15
|
+
def model_of(self, payload: dict[str, Any]) -> str | None: ...
|
|
16
|
+
|
|
17
|
+
def is_stream(self, payload: dict[str, Any]) -> bool: ...
|
|
18
|
+
|
|
19
|
+
def is_transformable(self, payload: dict[str, Any]) -> bool: ...
|
|
20
|
+
|
|
21
|
+
def iter_tool_results(self, payload: dict[str, Any]) -> Iterator[ToolResultRef]: ...
|
|
22
|
+
|
|
23
|
+
def segments(self, payload: dict[str, Any]) -> list[Segment]: ...
|
|
24
|
+
|
|
25
|
+
def system_text(self, payload: dict[str, Any]) -> str: ...
|
|
26
|
+
|
|
27
|
+
def first_user_text(self, payload: dict[str, Any]) -> str: ...
|
|
28
|
+
|
|
29
|
+
def tool_names(self, payload: dict[str, Any]) -> list[str]: ...
|
|
30
|
+
|
|
31
|
+
def used_tool_names(self, payload: dict[str, Any]) -> list[str]: ...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DictToolResultRef(ToolResultRef):
|
|
35
|
+
"""Edits a text-bearing ``content``/``output`` field inside a JSON block."""
|
|
36
|
+
|
|
37
|
+
__slots__ = ("_block", "_key")
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
block: dict[str, Any],
|
|
42
|
+
*,
|
|
43
|
+
message_index: int,
|
|
44
|
+
block_index: int | None,
|
|
45
|
+
tool_use_id: str,
|
|
46
|
+
tool_name: str | None,
|
|
47
|
+
is_error: bool,
|
|
48
|
+
key: str,
|
|
49
|
+
) -> None:
|
|
50
|
+
super().__init__(
|
|
51
|
+
message_index=message_index,
|
|
52
|
+
block_index=block_index,
|
|
53
|
+
tool_use_id=tool_use_id,
|
|
54
|
+
tool_name=tool_name,
|
|
55
|
+
is_error=is_error,
|
|
56
|
+
)
|
|
57
|
+
self._block = block
|
|
58
|
+
self._key = key
|
|
59
|
+
|
|
60
|
+
def get_text(self) -> str | None:
|
|
61
|
+
return content_text(self._block.get(self._key))
|
|
62
|
+
|
|
63
|
+
def set_text(self, new_text: str) -> None:
|
|
64
|
+
set_content_text(self._block, self._key, new_text)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class MappingToolResultRef(ToolResultRef):
|
|
68
|
+
"""Edits a string value on a mapping (Gemini functionResponse)."""
|
|
69
|
+
|
|
70
|
+
__slots__ = ("_block", "_key")
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
block: dict[str, Any],
|
|
75
|
+
*,
|
|
76
|
+
key: str,
|
|
77
|
+
message_index: int,
|
|
78
|
+
block_index: int | None,
|
|
79
|
+
tool_use_id: str,
|
|
80
|
+
tool_name: str | None,
|
|
81
|
+
is_error: bool,
|
|
82
|
+
) -> None:
|
|
83
|
+
super().__init__(
|
|
84
|
+
message_index=message_index,
|
|
85
|
+
block_index=block_index,
|
|
86
|
+
tool_use_id=tool_use_id,
|
|
87
|
+
tool_name=tool_name,
|
|
88
|
+
is_error=is_error,
|
|
89
|
+
)
|
|
90
|
+
self._block = block
|
|
91
|
+
self._key = key
|
|
92
|
+
|
|
93
|
+
def get_text(self) -> str | None:
|
|
94
|
+
value = self._block.get(self._key)
|
|
95
|
+
return value if isinstance(value, str) else None
|
|
96
|
+
|
|
97
|
+
def set_text(self, new_text: str) -> None:
|
|
98
|
+
self._block[self._key] = new_text
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def content_text(value: Any) -> str | None:
|
|
102
|
+
if isinstance(value, str):
|
|
103
|
+
return value
|
|
104
|
+
if isinstance(value, list):
|
|
105
|
+
parts = [
|
|
106
|
+
part.get("text", "")
|
|
107
|
+
for part in value
|
|
108
|
+
if isinstance(part, dict) and part.get("type") == "text"
|
|
109
|
+
]
|
|
110
|
+
return "".join(parts)
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def set_content_text(block: dict[str, Any], key: str, text: str) -> None:
|
|
115
|
+
value = block.get(key)
|
|
116
|
+
if isinstance(value, list):
|
|
117
|
+
remaining = [
|
|
118
|
+
part for part in value if not (isinstance(part, dict) and part.get("type") == "text")
|
|
119
|
+
]
|
|
120
|
+
block[key] = [{"type": "text", "text": text}, *remaining]
|
|
121
|
+
else:
|
|
122
|
+
block[key] = text
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def blocks_of(content: Any) -> list[dict[str, Any]]:
|
|
126
|
+
if isinstance(content, str):
|
|
127
|
+
return [{"type": "text", "text": content}]
|
|
128
|
+
if isinstance(content, list):
|
|
129
|
+
return [block for block in content if isinstance(block, dict)]
|
|
130
|
+
return []
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def text_of(value: Any) -> str:
|
|
134
|
+
if isinstance(value, str):
|
|
135
|
+
return value
|
|
136
|
+
if isinstance(value, list):
|
|
137
|
+
return "".join(
|
|
138
|
+
part.get("text", "")
|
|
139
|
+
for part in value
|
|
140
|
+
if isinstance(part, dict) and part.get("type") == "text"
|
|
141
|
+
)
|
|
142
|
+
return ""
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class PassthroughAdapter:
|
|
146
|
+
"""No structural access; used for native Gemini/Ollama until P6."""
|
|
147
|
+
|
|
148
|
+
provider: Provider
|
|
149
|
+
|
|
150
|
+
def __init__(self, provider: Provider) -> None:
|
|
151
|
+
self.provider = provider
|
|
152
|
+
|
|
153
|
+
def model_of(self, payload: dict[str, Any]) -> str | None:
|
|
154
|
+
model = payload.get("model")
|
|
155
|
+
return model if isinstance(model, str) else None
|
|
156
|
+
|
|
157
|
+
def is_stream(self, payload: dict[str, Any]) -> bool:
|
|
158
|
+
return bool(payload.get("stream"))
|
|
159
|
+
|
|
160
|
+
def is_transformable(self, payload: dict[str, Any]) -> bool:
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
def iter_tool_results(self, payload: dict[str, Any]) -> Iterator[ToolResultRef]:
|
|
164
|
+
return iter(())
|
|
165
|
+
|
|
166
|
+
def segments(self, payload: dict[str, Any]) -> list[Segment]:
|
|
167
|
+
return []
|
|
168
|
+
|
|
169
|
+
def system_text(self, payload: dict[str, Any]) -> str:
|
|
170
|
+
return ""
|
|
171
|
+
|
|
172
|
+
def first_user_text(self, payload: dict[str, Any]) -> str:
|
|
173
|
+
return ""
|
|
174
|
+
|
|
175
|
+
def tool_names(self, payload: dict[str, Any]) -> list[str]:
|
|
176
|
+
return []
|
|
177
|
+
|
|
178
|
+
def used_tool_names(self, payload: dict[str, Any]) -> list[str]:
|
|
179
|
+
return []
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
__all__ = [
|
|
183
|
+
"DictToolResultRef",
|
|
184
|
+
"MappingToolResultRef",
|
|
185
|
+
"PassthroughAdapter",
|
|
186
|
+
"ProviderAdapter",
|
|
187
|
+
"blocks_of",
|
|
188
|
+
"content_text",
|
|
189
|
+
"set_content_text",
|
|
190
|
+
"text_of",
|
|
191
|
+
]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Native Gemini ``generateContent`` adapter (SPEC section 11.4, P6-1).
|
|
2
|
+
|
|
3
|
+
Telemetry first: segments and tool-result handles are exposed so the analysis
|
|
4
|
+
layer can measure usage. Transforms only touch string leaves inside
|
|
5
|
+
``functionResponse`` parts, which is the byte-true subset.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Iterator
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ctxoptimizer.analysis.segments import Segment, make_segment
|
|
14
|
+
from ctxoptimizer.models import Provider, ToolResultRef
|
|
15
|
+
from ctxoptimizer.providers.base import MappingToolResultRef, text_of
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GeminiAdapter:
|
|
19
|
+
provider = Provider.GEMINI
|
|
20
|
+
|
|
21
|
+
def model_of(self, payload: dict[str, Any]) -> str | None:
|
|
22
|
+
model = payload.get("model")
|
|
23
|
+
return model if isinstance(model, str) else None
|
|
24
|
+
|
|
25
|
+
def is_stream(self, payload: dict[str, Any]) -> bool:
|
|
26
|
+
return bool(payload.get("stream"))
|
|
27
|
+
|
|
28
|
+
def is_transformable(self, payload: dict[str, Any]) -> bool:
|
|
29
|
+
return isinstance(payload.get("contents"), list)
|
|
30
|
+
|
|
31
|
+
def iter_tool_results(self, payload: dict[str, Any]) -> Iterator[ToolResultRef]:
|
|
32
|
+
for message_index, content in enumerate(payload.get("contents") or []):
|
|
33
|
+
if not isinstance(content, dict):
|
|
34
|
+
continue
|
|
35
|
+
for block_index, part in enumerate(content.get("parts") or []):
|
|
36
|
+
if not isinstance(part, dict):
|
|
37
|
+
continue
|
|
38
|
+
response = part.get("functionResponse")
|
|
39
|
+
if not isinstance(response, dict):
|
|
40
|
+
continue
|
|
41
|
+
name = str(response.get("name") or "")
|
|
42
|
+
handle = _response_handle(response)
|
|
43
|
+
if handle is None:
|
|
44
|
+
continue
|
|
45
|
+
parent, key, is_error = handle
|
|
46
|
+
yield MappingToolResultRef(
|
|
47
|
+
parent,
|
|
48
|
+
key=key,
|
|
49
|
+
message_index=message_index,
|
|
50
|
+
block_index=block_index,
|
|
51
|
+
tool_use_id=name,
|
|
52
|
+
tool_name=name or None,
|
|
53
|
+
is_error=is_error,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def segments(self, payload: dict[str, Any]) -> list[Segment]:
|
|
57
|
+
result: list[Segment] = []
|
|
58
|
+
for tool in payload.get("tools") or []:
|
|
59
|
+
if not isinstance(tool, dict):
|
|
60
|
+
continue
|
|
61
|
+
result.extend(
|
|
62
|
+
make_segment("tool_def", declaration)
|
|
63
|
+
for declaration in tool.get("functionDeclarations") or []
|
|
64
|
+
if isinstance(declaration, dict)
|
|
65
|
+
)
|
|
66
|
+
system = _parts(payload.get("systemInstruction"))
|
|
67
|
+
for index, part in enumerate(system):
|
|
68
|
+
result.append(make_segment("system", part, block_index=index))
|
|
69
|
+
for message_index, content in enumerate(payload.get("contents") or []):
|
|
70
|
+
if not isinstance(content, dict):
|
|
71
|
+
continue
|
|
72
|
+
role = str(content.get("role") or "")
|
|
73
|
+
for block_index, part in enumerate(content.get("parts") or []):
|
|
74
|
+
if isinstance(part, dict):
|
|
75
|
+
result.append(
|
|
76
|
+
make_segment(
|
|
77
|
+
"message",
|
|
78
|
+
{"role": role, **part},
|
|
79
|
+
msg_index=message_index,
|
|
80
|
+
block_index=block_index,
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
def system_text(self, payload: dict[str, Any]) -> str:
|
|
86
|
+
return text_of(_parts(payload.get("systemInstruction")))
|
|
87
|
+
|
|
88
|
+
def first_user_text(self, payload: dict[str, Any]) -> str:
|
|
89
|
+
for content in payload.get("contents") or []:
|
|
90
|
+
if isinstance(content, dict) and content.get("role") == "user":
|
|
91
|
+
return text_of(_parts(content))
|
|
92
|
+
return ""
|
|
93
|
+
|
|
94
|
+
def tool_names(self, payload: dict[str, Any]) -> list[str]:
|
|
95
|
+
names: list[str] = []
|
|
96
|
+
for tool in payload.get("tools") or []:
|
|
97
|
+
if not isinstance(tool, dict):
|
|
98
|
+
continue
|
|
99
|
+
names.extend(
|
|
100
|
+
declaration["name"]
|
|
101
|
+
for declaration in tool.get("functionDeclarations") or []
|
|
102
|
+
if isinstance(declaration, dict) and isinstance(declaration.get("name"), str)
|
|
103
|
+
)
|
|
104
|
+
return names
|
|
105
|
+
|
|
106
|
+
def used_tool_names(self, payload: dict[str, Any]) -> list[str]:
|
|
107
|
+
names: list[str] = []
|
|
108
|
+
for content in payload.get("contents") or []:
|
|
109
|
+
if not isinstance(content, dict):
|
|
110
|
+
continue
|
|
111
|
+
for part in content.get("parts") or []:
|
|
112
|
+
if not isinstance(part, dict):
|
|
113
|
+
continue
|
|
114
|
+
call = part.get("functionCall")
|
|
115
|
+
if isinstance(call, dict) and isinstance(call.get("name"), str):
|
|
116
|
+
names.append(call["name"])
|
|
117
|
+
return names
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _parts(value: Any) -> list[dict[str, Any]]:
|
|
121
|
+
if isinstance(value, dict):
|
|
122
|
+
parts = value.get("parts")
|
|
123
|
+
if isinstance(parts, list):
|
|
124
|
+
return [part for part in parts if isinstance(part, dict)]
|
|
125
|
+
return []
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _response_handle(
|
|
129
|
+
response: dict[str, Any],
|
|
130
|
+
) -> tuple[dict[str, Any], str, bool] | None:
|
|
131
|
+
inner = response.get("response")
|
|
132
|
+
if isinstance(inner, str):
|
|
133
|
+
return response, "response", False
|
|
134
|
+
if not isinstance(inner, dict):
|
|
135
|
+
return None
|
|
136
|
+
is_error = "error" in inner
|
|
137
|
+
for key in ("content", "result", "output", "text"):
|
|
138
|
+
if isinstance(inner.get(key), str):
|
|
139
|
+
return inner, key, is_error
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
__all__ = ["GeminiAdapter"]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Native Ollama ``/api/chat`` and ``/api/generate`` adapter (P6-2)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ctxoptimizer.analysis.segments import Segment, make_segment
|
|
9
|
+
from ctxoptimizer.models import Provider, ToolResultRef
|
|
10
|
+
from ctxoptimizer.providers.base import DictToolResultRef, text_of
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OllamaAdapter:
|
|
14
|
+
provider = Provider.OLLAMA
|
|
15
|
+
|
|
16
|
+
def model_of(self, payload: dict[str, Any]) -> str | None:
|
|
17
|
+
model = payload.get("model")
|
|
18
|
+
return model if isinstance(model, str) else None
|
|
19
|
+
|
|
20
|
+
def is_stream(self, payload: dict[str, Any]) -> bool:
|
|
21
|
+
# Ollama streams by default when ``stream`` is omitted.
|
|
22
|
+
return bool(payload.get("stream", True))
|
|
23
|
+
|
|
24
|
+
def is_transformable(self, payload: dict[str, Any]) -> bool:
|
|
25
|
+
return isinstance(payload.get("messages"), list) or isinstance(payload.get("prompt"), str)
|
|
26
|
+
|
|
27
|
+
def iter_tool_results(self, payload: dict[str, Any]) -> Iterator[ToolResultRef]:
|
|
28
|
+
for message_index, message in enumerate(payload.get("messages") or []):
|
|
29
|
+
if not isinstance(message, dict) or message.get("role") != "tool":
|
|
30
|
+
continue
|
|
31
|
+
yield DictToolResultRef(
|
|
32
|
+
message,
|
|
33
|
+
message_index=message_index,
|
|
34
|
+
block_index=None,
|
|
35
|
+
tool_use_id=str(message.get("tool_name") or ""),
|
|
36
|
+
tool_name=(
|
|
37
|
+
message.get("tool_name") if isinstance(message.get("tool_name"), str) else None
|
|
38
|
+
),
|
|
39
|
+
is_error=False,
|
|
40
|
+
key="content",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def segments(self, payload: dict[str, Any]) -> list[Segment]:
|
|
44
|
+
result: list[Segment] = []
|
|
45
|
+
for tool in payload.get("tools") or []:
|
|
46
|
+
if not isinstance(tool, dict):
|
|
47
|
+
continue
|
|
48
|
+
function = tool.get("function")
|
|
49
|
+
if isinstance(function, dict):
|
|
50
|
+
result.append(make_segment("tool_def", function))
|
|
51
|
+
system = payload.get("system")
|
|
52
|
+
if isinstance(system, str) and system:
|
|
53
|
+
result.append(make_segment("system", {"type": "text", "text": system}))
|
|
54
|
+
messages = payload.get("messages")
|
|
55
|
+
if isinstance(messages, list):
|
|
56
|
+
for message_index, message in enumerate(messages):
|
|
57
|
+
if not isinstance(message, dict):
|
|
58
|
+
continue
|
|
59
|
+
role = str(message.get("role") or "")
|
|
60
|
+
content = message.get("content")
|
|
61
|
+
if isinstance(content, str):
|
|
62
|
+
result.append(
|
|
63
|
+
make_segment(
|
|
64
|
+
"message",
|
|
65
|
+
{"role": role, "content": content},
|
|
66
|
+
msg_index=message_index,
|
|
67
|
+
block_index=0,
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
elif content is None and isinstance(message.get("tool_calls"), list):
|
|
71
|
+
result.append(
|
|
72
|
+
make_segment(
|
|
73
|
+
"message",
|
|
74
|
+
{"role": role, "tool_calls": message["tool_calls"]},
|
|
75
|
+
msg_index=message_index,
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
elif isinstance(payload.get("prompt"), str):
|
|
79
|
+
result.append(
|
|
80
|
+
make_segment(
|
|
81
|
+
"message",
|
|
82
|
+
{"role": "user", "content": payload["prompt"]},
|
|
83
|
+
msg_index=0,
|
|
84
|
+
block_index=0,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
def system_text(self, payload: dict[str, Any]) -> str:
|
|
90
|
+
system = payload.get("system")
|
|
91
|
+
if isinstance(system, str):
|
|
92
|
+
return system
|
|
93
|
+
parts = [
|
|
94
|
+
text_of(message.get("content"))
|
|
95
|
+
for message in payload.get("messages") or []
|
|
96
|
+
if isinstance(message, dict) and message.get("role") == "system"
|
|
97
|
+
]
|
|
98
|
+
return "\n".join(part for part in parts if part)
|
|
99
|
+
|
|
100
|
+
def first_user_text(self, payload: dict[str, Any]) -> str:
|
|
101
|
+
prompt = payload.get("prompt")
|
|
102
|
+
if isinstance(prompt, str):
|
|
103
|
+
return prompt
|
|
104
|
+
for message in payload.get("messages") or []:
|
|
105
|
+
if isinstance(message, dict) and message.get("role") == "user":
|
|
106
|
+
return text_of(message.get("content"))
|
|
107
|
+
return ""
|
|
108
|
+
|
|
109
|
+
def tool_names(self, payload: dict[str, Any]) -> list[str]:
|
|
110
|
+
names: list[str] = []
|
|
111
|
+
for tool in payload.get("tools") or []:
|
|
112
|
+
if not isinstance(tool, dict):
|
|
113
|
+
continue
|
|
114
|
+
function = tool.get("function")
|
|
115
|
+
if isinstance(function, dict) and isinstance(function.get("name"), str):
|
|
116
|
+
names.append(function["name"])
|
|
117
|
+
return names
|
|
118
|
+
|
|
119
|
+
def used_tool_names(self, payload: dict[str, Any]) -> list[str]:
|
|
120
|
+
names: list[str] = []
|
|
121
|
+
for message in payload.get("messages") or []:
|
|
122
|
+
if not isinstance(message, dict):
|
|
123
|
+
continue
|
|
124
|
+
for call in message.get("tool_calls") or []:
|
|
125
|
+
if not isinstance(call, dict):
|
|
126
|
+
continue
|
|
127
|
+
function = call.get("function")
|
|
128
|
+
if isinstance(function, dict) and isinstance(function.get("name"), str):
|
|
129
|
+
names.append(function["name"])
|
|
130
|
+
return names
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
__all__ = ["OllamaAdapter"]
|