elkpi-oxa 1.0.0__tar.gz

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.
Files changed (58) hide show
  1. elkpi_oxa-1.0.0/.gitignore +45 -0
  2. elkpi_oxa-1.0.0/PKG-INFO +82 -0
  3. elkpi_oxa-1.0.0/README.md +63 -0
  4. elkpi_oxa-1.0.0/pyproject.toml +67 -0
  5. elkpi_oxa-1.0.0/src/oxa/__init__.py +52 -0
  6. elkpi_oxa-1.0.0/src/oxa/anthropic/__init__.py +1 -0
  7. elkpi_oxa-1.0.0/src/oxa/anthropic/messages/__init__.py +72 -0
  8. elkpi_oxa-1.0.0/src/oxa/anthropic/messages/constants.py +35 -0
  9. elkpi_oxa-1.0.0/src/oxa/anthropic/messages/decode.py +203 -0
  10. elkpi_oxa-1.0.0/src/oxa/anthropic/messages/encode.py +396 -0
  11. elkpi_oxa-1.0.0/src/oxa/anthropic/messages/normalize.py +309 -0
  12. elkpi_oxa-1.0.0/src/oxa/anthropic/messages/streamin.py +337 -0
  13. elkpi_oxa-1.0.0/src/oxa/anthropic/messages/streamout.py +226 -0
  14. elkpi_oxa-1.0.0/src/oxa/ir/__init__.py +161 -0
  15. elkpi_oxa-1.0.0/src/oxa/ir/checker.py +202 -0
  16. elkpi_oxa-1.0.0/src/oxa/ir/codec.py +411 -0
  17. elkpi_oxa-1.0.0/src/oxa/ir/constants.py +45 -0
  18. elkpi_oxa-1.0.0/src/oxa/ir/loss.py +38 -0
  19. elkpi_oxa-1.0.0/src/oxa/ir/types.py +248 -0
  20. elkpi_oxa-1.0.0/src/oxa/modelmap/__init__.py +29 -0
  21. elkpi_oxa-1.0.0/src/oxa/openai/__init__.py +1 -0
  22. elkpi_oxa-1.0.0/src/oxa/openai/chatcompletions/__init__.py +52 -0
  23. elkpi_oxa-1.0.0/src/oxa/openai/chatcompletions/constants.py +23 -0
  24. elkpi_oxa-1.0.0/src/oxa/openai/chatcompletions/decode.py +264 -0
  25. elkpi_oxa-1.0.0/src/oxa/openai/chatcompletions/encode.py +218 -0
  26. elkpi_oxa-1.0.0/src/oxa/openai/chatcompletions/normalize.py +458 -0
  27. elkpi_oxa-1.0.0/src/oxa/openai/chatcompletions/streamin.py +228 -0
  28. elkpi_oxa-1.0.0/src/oxa/openai/chatcompletions/streamout.py +259 -0
  29. elkpi_oxa-1.0.0/src/oxa/openai/responses/__init__.py +86 -0
  30. elkpi_oxa-1.0.0/src/oxa/openai/responses/constants.py +44 -0
  31. elkpi_oxa-1.0.0/src/oxa/openai/responses/decode.py +332 -0
  32. elkpi_oxa-1.0.0/src/oxa/openai/responses/encode.py +241 -0
  33. elkpi_oxa-1.0.0/src/oxa/openai/responses/normalize.py +481 -0
  34. elkpi_oxa-1.0.0/src/oxa/openai/responses/streamin.py +447 -0
  35. elkpi_oxa-1.0.0/src/oxa/openai/responses/streamout.py +489 -0
  36. elkpi_oxa-1.0.0/src/oxa/py.typed +0 -0
  37. elkpi_oxa-1.0.0/src/oxa/sse/__init__.py +94 -0
  38. elkpi_oxa-1.0.0/src/oxa/vectest/__init__.py +28 -0
  39. elkpi_oxa-1.0.0/src/oxa/vectest/compare.py +136 -0
  40. elkpi_oxa-1.0.0/src/oxa/vectest/load.py +117 -0
  41. elkpi_oxa-1.0.0/src/oxa/vectest/runner.py +159 -0
  42. elkpi_oxa-1.0.0/tests/__init__.py +0 -0
  43. elkpi_oxa-1.0.0/tests/test_anthropic_nonstream.py +114 -0
  44. elkpi_oxa-1.0.0/tests/test_anthropic_vectors.py +40 -0
  45. elkpi_oxa-1.0.0/tests/test_chatcompletions_nonstream.py +135 -0
  46. elkpi_oxa-1.0.0/tests/test_chatcompletions_vectors.py +40 -0
  47. elkpi_oxa-1.0.0/tests/test_cross_vectors.py +79 -0
  48. elkpi_oxa-1.0.0/tests/test_fragment_corpus.py +241 -0
  49. elkpi_oxa-1.0.0/tests/test_ir_checker.py +275 -0
  50. elkpi_oxa-1.0.0/tests/test_ir_codec.py +157 -0
  51. elkpi_oxa-1.0.0/tests/test_modelmap.py +30 -0
  52. elkpi_oxa-1.0.0/tests/test_responses_nonstream.py +98 -0
  53. elkpi_oxa-1.0.0/tests/test_responses_vectors.py +40 -0
  54. elkpi_oxa-1.0.0/tests/test_smoke.py +12 -0
  55. elkpi_oxa-1.0.0/tests/test_sse.py +78 -0
  56. elkpi_oxa-1.0.0/tests/test_stream_unit.py +78 -0
  57. elkpi_oxa-1.0.0/tests/test_stream_vectors.py +64 -0
  58. elkpi_oxa-1.0.0/tests/test_vectest.py +64 -0
@@ -0,0 +1,45 @@
1
+ # Go artifacts (per-language, under go/)
2
+ go/bin/
3
+ go/dist/
4
+ go/vendor/
5
+ go/*.test
6
+ go/*.exe
7
+ go/go.work.sum
8
+
9
+ # Test and coverage artifacts
10
+ *.out
11
+ *.test
12
+ coverage.txt
13
+ coverage.html
14
+ *.prof
15
+
16
+ # Editors and OS
17
+ .idea/
18
+ .vscode/
19
+ .DS_Store
20
+ *.swp
21
+ *.swo
22
+ *~
23
+
24
+ # veccheck binary (built locally, never committed)
25
+ go/veccheck
26
+
27
+ # Rust artifacts (per-language, under rust/)
28
+ /rust/target/
29
+ rust/**/*.rs.bk
30
+
31
+ # Python artifacts (per-language, under python/)
32
+ python/.venv/
33
+ python/**/__pycache__/
34
+ python/**/*.py[cod]
35
+ python/.pytest_cache/
36
+ python/.mypy_cache/
37
+ python/.ruff_cache/
38
+ python/dist/
39
+ python/build/
40
+ python/*.egg-info/
41
+
42
+ # C++ artifacts (per-language, under cpp/)
43
+ cpp/build/
44
+ cpp/Testing/
45
+
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.5
2
+ Name: elkpi-oxa
3
+ Version: 1.0.0
4
+ Summary: Pure, in-process protocol-conversion library for OpenAI and Anthropic
5
+ Author: oxa contributors
6
+ License-Expression: Apache-2.0
7
+ Classifier: Development Status :: 5 - Production/Stable
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: Apache Software License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+
20
+ # oxa — Python Reference Implementation
21
+
22
+ Pure, in-process protocol-conversion library for OpenAI Chat Completions,
23
+ OpenAI Responses, and Anthropic Messages in Python.
24
+
25
+ Like the Go and Rust implementations, oxa in Python is not a proxy, HTTP
26
+ client, router, retry layer, authentication service, or model capability
27
+ database. It provides deterministic, pure conversions between protocol wire
28
+ structures and oxa's shared Intermediate Representation (IR).
29
+
30
+ ## Architecture
31
+
32
+ - **Hub-and-spoke**: Spoke packages (`oxa.openai.chatcompletions`,
33
+ `oxa.openai.responses`, `oxa.anthropic.messages`) implement only `face → IR`
34
+ and `IR → face`. No direct face-to-face conversions.
35
+ - **Zero runtime dependencies**: Pure Python standard library only
36
+ (`dataclasses`, `typing`, `json`, `enum`, etc.).
37
+ - **Opaque tool inputs (INV-1)**: Tool call arguments and streaming delta
38
+ fragments remain unparsed raw JSON text.
39
+ - **Shared Golden Vectors**: Conforms to the exact same shared `vectors/` golden
40
+ suite as Go and Rust.
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install elkpi-oxa
46
+ ```
47
+
48
+ Import in Python:
49
+
50
+ ```python
51
+ import oxa
52
+ from oxa.openai.chatcompletions import decode_request
53
+ ```
54
+
55
+ ## Development
56
+
57
+ The project uses [`uv`](https://docs.astral.sh/uv/) for development and
58
+ environment management:
59
+
60
+ ```bash
61
+ cd python
62
+
63
+ # Sync virtualenv and development dependencies
64
+ uv sync
65
+
66
+ # Run tests
67
+ uv run pytest
68
+
69
+ # Type check
70
+ uv run mypy
71
+
72
+ # Lint & format check
73
+ uv run ruff check .
74
+ uv run ruff format --check .
75
+ ```
76
+
77
+ ## Vectors Location Convention
78
+
79
+ Test harnesses locate the shared `vectors/` suite by walking up parent
80
+ directories from `python/` until finding a directory containing both `vectors/`
81
+ and `.git/`. Tests skip cleanly if the repository root is absent (e.g. when
82
+ building a standalone package archive).
@@ -0,0 +1,63 @@
1
+ # oxa — Python Reference Implementation
2
+
3
+ Pure, in-process protocol-conversion library for OpenAI Chat Completions,
4
+ OpenAI Responses, and Anthropic Messages in Python.
5
+
6
+ Like the Go and Rust implementations, oxa in Python is not a proxy, HTTP
7
+ client, router, retry layer, authentication service, or model capability
8
+ database. It provides deterministic, pure conversions between protocol wire
9
+ structures and oxa's shared Intermediate Representation (IR).
10
+
11
+ ## Architecture
12
+
13
+ - **Hub-and-spoke**: Spoke packages (`oxa.openai.chatcompletions`,
14
+ `oxa.openai.responses`, `oxa.anthropic.messages`) implement only `face → IR`
15
+ and `IR → face`. No direct face-to-face conversions.
16
+ - **Zero runtime dependencies**: Pure Python standard library only
17
+ (`dataclasses`, `typing`, `json`, `enum`, etc.).
18
+ - **Opaque tool inputs (INV-1)**: Tool call arguments and streaming delta
19
+ fragments remain unparsed raw JSON text.
20
+ - **Shared Golden Vectors**: Conforms to the exact same shared `vectors/` golden
21
+ suite as Go and Rust.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pip install elkpi-oxa
27
+ ```
28
+
29
+ Import in Python:
30
+
31
+ ```python
32
+ import oxa
33
+ from oxa.openai.chatcompletions import decode_request
34
+ ```
35
+
36
+ ## Development
37
+
38
+ The project uses [`uv`](https://docs.astral.sh/uv/) for development and
39
+ environment management:
40
+
41
+ ```bash
42
+ cd python
43
+
44
+ # Sync virtualenv and development dependencies
45
+ uv sync
46
+
47
+ # Run tests
48
+ uv run pytest
49
+
50
+ # Type check
51
+ uv run mypy
52
+
53
+ # Lint & format check
54
+ uv run ruff check .
55
+ uv run ruff format --check .
56
+ ```
57
+
58
+ ## Vectors Location Convention
59
+
60
+ Test harnesses locate the shared `vectors/` suite by walking up parent
61
+ directories from `python/` until finding a directory containing both `vectors/`
62
+ and `.git/`. Tests skip cleanly if the repository root is absent (e.g. when
63
+ building a standalone package archive).
@@ -0,0 +1,67 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "elkpi-oxa"
7
+ version = "1.0.0"
8
+ description = "Pure, in-process protocol-conversion library for OpenAI and Anthropic"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "Apache-2.0"
12
+ authors = [
13
+ { name = "oxa contributors" },
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 5 - Production/Stable",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: Apache Software License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Software Development :: Libraries :: Python Modules",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = []
28
+
29
+ [dependency-groups]
30
+ dev = [
31
+ "pytest>=8.0.0",
32
+ "ruff>=0.9.0",
33
+ "mypy>=1.14.0",
34
+ ]
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/oxa"]
38
+
39
+ [tool.ruff]
40
+ line-length = 100
41
+ target-version = "py310"
42
+
43
+ [tool.ruff.lint]
44
+ select = [
45
+ "E", # pycodestyle errors
46
+ "F", # pyflakes
47
+ "I", # isort
48
+ "N", # pep8-naming
49
+ "W", # pycodestyle warnings
50
+ "UP", # pyupgrade
51
+ "B", # flake8-bugbear
52
+ "C4", # flake8-comprehensions
53
+ ]
54
+ ignore = []
55
+
56
+ [tool.ruff.lint.isort]
57
+ known-first-party = ["oxa"]
58
+
59
+ [tool.mypy]
60
+ python_version = "3.10"
61
+ strict = true
62
+ packages = ["oxa"]
63
+ mypy_path = "src"
64
+
65
+ [tool.pytest.ini_options]
66
+ testpaths = ["tests"]
67
+ pythonpath = ["src"]
@@ -0,0 +1,52 @@
1
+ """oxa — pure, in-process protocol-conversion library for OpenAI and Anthropic."""
2
+
3
+ from oxa.ir import (
4
+ Block,
5
+ Event,
6
+ EventStream,
7
+ ImageBlock,
8
+ InputJsonDelta,
9
+ Loss,
10
+ Message,
11
+ MessageDelta,
12
+ MessageDone,
13
+ MessageStart,
14
+ Params,
15
+ Request,
16
+ Response,
17
+ TextBlock,
18
+ TextDelta,
19
+ Tool,
20
+ ToolChoice,
21
+ ToolResultBlock,
22
+ ToolUseBlock,
23
+ Usage,
24
+ )
25
+ from oxa.modelmap import Table
26
+
27
+ __version__ = "1.0.0"
28
+
29
+ __all__ = [
30
+ "__version__",
31
+ "Request",
32
+ "Response",
33
+ "Message",
34
+ "Block",
35
+ "TextBlock",
36
+ "ImageBlock",
37
+ "ToolUseBlock",
38
+ "ToolResultBlock",
39
+ "Tool",
40
+ "ToolChoice",
41
+ "Params",
42
+ "Usage",
43
+ "Loss",
44
+ "Event",
45
+ "EventStream",
46
+ "MessageStart",
47
+ "MessageDelta",
48
+ "MessageDone",
49
+ "TextDelta",
50
+ "InputJsonDelta",
51
+ "Table",
52
+ ]
@@ -0,0 +1 @@
1
+ """Anthropic protocol faces."""
@@ -0,0 +1,72 @@
1
+ """Anthropic Messages face implementation (spec/12)."""
2
+
3
+ from oxa.anthropic.messages.constants import (
4
+ BLOCK_TYPE_IMAGE,
5
+ BLOCK_TYPE_TEXT,
6
+ BLOCK_TYPE_TOOL_RESULT,
7
+ BLOCK_TYPE_TOOL_USE,
8
+ DELTA_TYPE_INPUT_JSON_DELTA,
9
+ DELTA_TYPE_TEXT_DELTA,
10
+ EVENT_TYPE_CONTENT_BLOCK_DELTA,
11
+ EVENT_TYPE_CONTENT_BLOCK_START,
12
+ EVENT_TYPE_CONTENT_BLOCK_STOP,
13
+ EVENT_TYPE_MESSAGE_DELTA,
14
+ EVENT_TYPE_MESSAGE_START,
15
+ EVENT_TYPE_MESSAGE_STOP,
16
+ ROLE_ASSISTANT,
17
+ ROLE_USER,
18
+ SOURCE_TYPE_BASE64,
19
+ SOURCE_TYPE_URL,
20
+ STOP_REASON_END_TURN,
21
+ STOP_REASON_MAX_TOKENS,
22
+ STOP_REASON_REFUSAL,
23
+ STOP_REASON_STOP_SEQUENCE,
24
+ STOP_REASON_TOOL_USE,
25
+ TOOL_CHOICE_TYPE_ANY,
26
+ TOOL_CHOICE_TYPE_AUTO,
27
+ TOOL_CHOICE_TYPE_NONE,
28
+ TOOL_CHOICE_TYPE_TOOL,
29
+ TYPE_MESSAGE,
30
+ )
31
+ from oxa.anthropic.messages.decode import decode_request, decode_response
32
+ from oxa.anthropic.messages.encode import encode_request, encode_response
33
+ from oxa.anthropic.messages.streamin import StreamDecoder
34
+ from oxa.anthropic.messages.streamout import StreamEncoder
35
+
36
+ __all__ = [
37
+ # Constants
38
+ "ROLE_USER",
39
+ "ROLE_ASSISTANT",
40
+ "BLOCK_TYPE_TEXT",
41
+ "BLOCK_TYPE_IMAGE",
42
+ "BLOCK_TYPE_TOOL_USE",
43
+ "BLOCK_TYPE_TOOL_RESULT",
44
+ "SOURCE_TYPE_BASE64",
45
+ "SOURCE_TYPE_URL",
46
+ "TOOL_CHOICE_TYPE_AUTO",
47
+ "TOOL_CHOICE_TYPE_ANY",
48
+ "TOOL_CHOICE_TYPE_NONE",
49
+ "TOOL_CHOICE_TYPE_TOOL",
50
+ "STOP_REASON_END_TURN",
51
+ "STOP_REASON_MAX_TOKENS",
52
+ "STOP_REASON_STOP_SEQUENCE",
53
+ "STOP_REASON_TOOL_USE",
54
+ "STOP_REASON_REFUSAL",
55
+ "EVENT_TYPE_MESSAGE_START",
56
+ "EVENT_TYPE_CONTENT_BLOCK_START",
57
+ "EVENT_TYPE_CONTENT_BLOCK_DELTA",
58
+ "EVENT_TYPE_CONTENT_BLOCK_STOP",
59
+ "EVENT_TYPE_MESSAGE_DELTA",
60
+ "EVENT_TYPE_MESSAGE_STOP",
61
+ "DELTA_TYPE_TEXT_DELTA",
62
+ "DELTA_TYPE_INPUT_JSON_DELTA",
63
+ "TYPE_MESSAGE",
64
+ # Functions
65
+ "decode_request",
66
+ "decode_response",
67
+ "encode_request",
68
+ "encode_response",
69
+ # Streaming Classes
70
+ "StreamDecoder",
71
+ "StreamEncoder",
72
+ ]
@@ -0,0 +1,35 @@
1
+ """Anthropic Messages wire protocol constants (spec/12)."""
2
+
3
+ ROLE_USER = "user"
4
+ ROLE_ASSISTANT = "assistant"
5
+
6
+ BLOCK_TYPE_TEXT = "text"
7
+ BLOCK_TYPE_IMAGE = "image"
8
+ BLOCK_TYPE_TOOL_USE = "tool_use"
9
+ BLOCK_TYPE_TOOL_RESULT = "tool_result"
10
+
11
+ SOURCE_TYPE_BASE64 = "base64"
12
+ SOURCE_TYPE_URL = "url"
13
+
14
+ TOOL_CHOICE_TYPE_AUTO = "auto"
15
+ TOOL_CHOICE_TYPE_ANY = "any"
16
+ TOOL_CHOICE_TYPE_NONE = "none"
17
+ TOOL_CHOICE_TYPE_TOOL = "tool"
18
+
19
+ STOP_REASON_END_TURN = "end_turn"
20
+ STOP_REASON_MAX_TOKENS = "max_tokens"
21
+ STOP_REASON_STOP_SEQUENCE = "stop_sequence"
22
+ STOP_REASON_TOOL_USE = "tool_use"
23
+ STOP_REASON_REFUSAL = "refusal"
24
+
25
+ EVENT_TYPE_MESSAGE_START = "message_start"
26
+ EVENT_TYPE_CONTENT_BLOCK_START = "content_block_start"
27
+ EVENT_TYPE_CONTENT_BLOCK_DELTA = "content_block_delta"
28
+ EVENT_TYPE_CONTENT_BLOCK_STOP = "content_block_stop"
29
+ EVENT_TYPE_MESSAGE_DELTA = "message_delta"
30
+ EVENT_TYPE_MESSAGE_STOP = "message_stop"
31
+
32
+ DELTA_TYPE_TEXT_DELTA = "text_delta"
33
+ DELTA_TYPE_INPUT_JSON_DELTA = "input_json_delta"
34
+
35
+ TYPE_MESSAGE = "message"
@@ -0,0 +1,203 @@
1
+ """Anthropic Messages face → IR conversions (spec/12 §4)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from oxa.anthropic.messages.constants import (
9
+ ROLE_ASSISTANT as AN_ROLE_ASSISTANT,
10
+ ROLE_USER as AN_ROLE_USER,
11
+ STOP_REASON_END_TURN,
12
+ STOP_REASON_MAX_TOKENS,
13
+ STOP_REASON_REFUSAL,
14
+ STOP_REASON_STOP_SEQUENCE,
15
+ STOP_REASON_TOOL_USE,
16
+ )
17
+ from oxa.anthropic.messages.normalize import (
18
+ decode_block,
19
+ decode_content,
20
+ decode_system,
21
+ decode_tool_choice,
22
+ loss,
23
+ require_json_object,
24
+ )
25
+ from oxa.ir import (
26
+ LOSS_UNMAPPED_FIELD,
27
+ LOSS_UNMAPPED_VALUE,
28
+ ROLE_ASSISTANT,
29
+ ROLE_USER,
30
+ STOP_END_TURN,
31
+ STOP_MAX_TOKENS,
32
+ STOP_OTHER,
33
+ STOP_REFUSAL,
34
+ STOP_STOP_SEQUENCE,
35
+ STOP_TOOL_USE,
36
+ Block,
37
+ Loss,
38
+ Message as IrMessage,
39
+ Params,
40
+ Request,
41
+ Response,
42
+ Tool,
43
+ Usage,
44
+ )
45
+ from oxa.modelmap import Table
46
+
47
+
48
+ def decode_request(
49
+ wire: dict[str, Any],
50
+ raw_text: str = "",
51
+ table: Table | None = None,
52
+ ) -> tuple[Request, list[Loss]]:
53
+ """Converts an Anthropic Messages wire request to the IR (face → IR)."""
54
+ losses: list[Loss] = []
55
+
56
+ max_tokens = wire.get("max_tokens")
57
+ if max_tokens is None or max_tokens <= 0:
58
+ raise ValueError("anthropic: max_tokens is required and must be positive")
59
+
60
+ if wire.get("metadata") is not None:
61
+ losses.append(
62
+ loss(
63
+ "metadata",
64
+ "metadata",
65
+ LOSS_UNMAPPED_FIELD,
66
+ "Anthropic request metadata (user_id) has no IR equivalent in v1.",
67
+ )
68
+ )
69
+
70
+ model = str(wire.get("model", ""))
71
+ if table is not None:
72
+ model = table.map(model)
73
+
74
+ tools: list[Tool] | None = None
75
+ if "tools" in wire and wire["tools"] is not None:
76
+ tools = []
77
+ for index, tool in enumerate(wire["tools"]):
78
+ schema = tool.get("input_schema")
79
+ require_json_object(schema, f"tools[{index}].input_schema")
80
+ desc = tool.get("description")
81
+ tools.append(
82
+ Tool(
83
+ name=tool["name"],
84
+ description=desc if desc else None,
85
+ input_schema=schema,
86
+ )
87
+ )
88
+
89
+ tool_choice, tc_losses = decode_tool_choice(wire.get("tool_choice"))
90
+ losses.extend(tc_losses)
91
+
92
+ system, sys_losses = decode_system(wire.get("system"))
93
+ losses.extend(sys_losses)
94
+
95
+ raw_messages = wire.get("messages", [])
96
+ if not raw_messages:
97
+ raise ValueError("anthropic: request carries no messages")
98
+
99
+ messages: list[IrMessage] = []
100
+ for index, msg in enumerate(raw_messages):
101
+ role_str = msg.get("role", "")
102
+ if role_str == AN_ROLE_USER:
103
+ role = ROLE_USER
104
+ elif role_str == AN_ROLE_ASSISTANT:
105
+ role = ROLE_ASSISTANT
106
+ else:
107
+ raise ValueError(f"anthropic: messages[{index}]: unknown role {role_str!r}")
108
+
109
+ content_path = f"messages[{index}].content"
110
+ blocks, b_losses = decode_content(msg.get("content"), content_path)
111
+ losses.extend(b_losses)
112
+ messages.append(IrMessage(role=role, content=blocks))
113
+
114
+ stop = wire.get("stop_sequences")
115
+ stop_sequences = [s for s in stop if s] if isinstance(stop, list) and stop else None
116
+
117
+ params = Params(
118
+ temperature=wire.get("temperature"),
119
+ top_p=wire.get("top_p"),
120
+ max_tokens=max_tokens,
121
+ stop_sequences=stop_sequences,
122
+ )
123
+
124
+ return (
125
+ Request(
126
+ model=model,
127
+ messages=messages,
128
+ system=system,
129
+ tools=tools if tools else None,
130
+ tool_choice=tool_choice,
131
+ params=params,
132
+ ),
133
+ losses,
134
+ )
135
+
136
+
137
+ def decode_response(
138
+ wire: dict[str, Any],
139
+ raw_text: str = "",
140
+ table: Table | None = None,
141
+ ) -> tuple[Response, list[Loss]]:
142
+ """Converts an Anthropic Messages wire response to the IR (face → IR)."""
143
+ losses: list[Loss] = []
144
+
145
+ content_raw = wire.get("content", [])
146
+ content: list[Block] = []
147
+ for index, block in enumerate(content_raw):
148
+ decoded, b_losses, mapped = decode_block(block, f"content[{index}]", raw_text)
149
+ if mapped:
150
+ content.extend(decoded)
151
+ losses.extend(b_losses)
152
+
153
+ stop_reason, stop_loss = decode_stop_reason(wire.get("stop_reason"))
154
+ if stop_loss is not None:
155
+ losses.append(stop_loss)
156
+
157
+ model = str(wire.get("model", ""))
158
+ if table is not None:
159
+ model = table.map(model)
160
+
161
+ usage_raw = wire.get("usage", {})
162
+ usage = Usage(
163
+ input_tokens=int(usage_raw.get("input_tokens", 0)),
164
+ output_tokens=int(usage_raw.get("output_tokens", 0)),
165
+ )
166
+
167
+ stop_seq = wire.get("stop_sequence")
168
+
169
+ return (
170
+ Response(
171
+ id=str(wire.get("id", "")),
172
+ model=model,
173
+ content=content,
174
+ stop_reason=stop_reason,
175
+ usage=usage,
176
+ stop_sequence=stop_seq if stop_seq else None,
177
+ ),
178
+ losses,
179
+ )
180
+
181
+
182
+ def decode_stop_reason(stop: Any) -> tuple[str, Loss | None]:
183
+ if stop == STOP_REASON_END_TURN:
184
+ return STOP_END_TURN, None
185
+ if stop == STOP_REASON_MAX_TOKENS:
186
+ return STOP_MAX_TOKENS, None
187
+ if stop == STOP_REASON_STOP_SEQUENCE:
188
+ return STOP_STOP_SEQUENCE, None
189
+ if stop == STOP_REASON_TOOL_USE:
190
+ return STOP_TOOL_USE, None
191
+ if stop == STOP_REASON_REFUSAL:
192
+ return STOP_REFUSAL, None
193
+ if not stop:
194
+ raise ValueError("anthropic: stop_reason is missing")
195
+ return (
196
+ STOP_OTHER,
197
+ loss(
198
+ "stop_reason",
199
+ "stop_reason",
200
+ LOSS_UNMAPPED_VALUE,
201
+ f"Anthropic stop_reason {stop!r} has no IR equivalent",
202
+ ),
203
+ )