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.
- elkpi_oxa-1.0.0/.gitignore +45 -0
- elkpi_oxa-1.0.0/PKG-INFO +82 -0
- elkpi_oxa-1.0.0/README.md +63 -0
- elkpi_oxa-1.0.0/pyproject.toml +67 -0
- elkpi_oxa-1.0.0/src/oxa/__init__.py +52 -0
- elkpi_oxa-1.0.0/src/oxa/anthropic/__init__.py +1 -0
- elkpi_oxa-1.0.0/src/oxa/anthropic/messages/__init__.py +72 -0
- elkpi_oxa-1.0.0/src/oxa/anthropic/messages/constants.py +35 -0
- elkpi_oxa-1.0.0/src/oxa/anthropic/messages/decode.py +203 -0
- elkpi_oxa-1.0.0/src/oxa/anthropic/messages/encode.py +396 -0
- elkpi_oxa-1.0.0/src/oxa/anthropic/messages/normalize.py +309 -0
- elkpi_oxa-1.0.0/src/oxa/anthropic/messages/streamin.py +337 -0
- elkpi_oxa-1.0.0/src/oxa/anthropic/messages/streamout.py +226 -0
- elkpi_oxa-1.0.0/src/oxa/ir/__init__.py +161 -0
- elkpi_oxa-1.0.0/src/oxa/ir/checker.py +202 -0
- elkpi_oxa-1.0.0/src/oxa/ir/codec.py +411 -0
- elkpi_oxa-1.0.0/src/oxa/ir/constants.py +45 -0
- elkpi_oxa-1.0.0/src/oxa/ir/loss.py +38 -0
- elkpi_oxa-1.0.0/src/oxa/ir/types.py +248 -0
- elkpi_oxa-1.0.0/src/oxa/modelmap/__init__.py +29 -0
- elkpi_oxa-1.0.0/src/oxa/openai/__init__.py +1 -0
- elkpi_oxa-1.0.0/src/oxa/openai/chatcompletions/__init__.py +52 -0
- elkpi_oxa-1.0.0/src/oxa/openai/chatcompletions/constants.py +23 -0
- elkpi_oxa-1.0.0/src/oxa/openai/chatcompletions/decode.py +264 -0
- elkpi_oxa-1.0.0/src/oxa/openai/chatcompletions/encode.py +218 -0
- elkpi_oxa-1.0.0/src/oxa/openai/chatcompletions/normalize.py +458 -0
- elkpi_oxa-1.0.0/src/oxa/openai/chatcompletions/streamin.py +228 -0
- elkpi_oxa-1.0.0/src/oxa/openai/chatcompletions/streamout.py +259 -0
- elkpi_oxa-1.0.0/src/oxa/openai/responses/__init__.py +86 -0
- elkpi_oxa-1.0.0/src/oxa/openai/responses/constants.py +44 -0
- elkpi_oxa-1.0.0/src/oxa/openai/responses/decode.py +332 -0
- elkpi_oxa-1.0.0/src/oxa/openai/responses/encode.py +241 -0
- elkpi_oxa-1.0.0/src/oxa/openai/responses/normalize.py +481 -0
- elkpi_oxa-1.0.0/src/oxa/openai/responses/streamin.py +447 -0
- elkpi_oxa-1.0.0/src/oxa/openai/responses/streamout.py +489 -0
- elkpi_oxa-1.0.0/src/oxa/py.typed +0 -0
- elkpi_oxa-1.0.0/src/oxa/sse/__init__.py +94 -0
- elkpi_oxa-1.0.0/src/oxa/vectest/__init__.py +28 -0
- elkpi_oxa-1.0.0/src/oxa/vectest/compare.py +136 -0
- elkpi_oxa-1.0.0/src/oxa/vectest/load.py +117 -0
- elkpi_oxa-1.0.0/src/oxa/vectest/runner.py +159 -0
- elkpi_oxa-1.0.0/tests/__init__.py +0 -0
- elkpi_oxa-1.0.0/tests/test_anthropic_nonstream.py +114 -0
- elkpi_oxa-1.0.0/tests/test_anthropic_vectors.py +40 -0
- elkpi_oxa-1.0.0/tests/test_chatcompletions_nonstream.py +135 -0
- elkpi_oxa-1.0.0/tests/test_chatcompletions_vectors.py +40 -0
- elkpi_oxa-1.0.0/tests/test_cross_vectors.py +79 -0
- elkpi_oxa-1.0.0/tests/test_fragment_corpus.py +241 -0
- elkpi_oxa-1.0.0/tests/test_ir_checker.py +275 -0
- elkpi_oxa-1.0.0/tests/test_ir_codec.py +157 -0
- elkpi_oxa-1.0.0/tests/test_modelmap.py +30 -0
- elkpi_oxa-1.0.0/tests/test_responses_nonstream.py +98 -0
- elkpi_oxa-1.0.0/tests/test_responses_vectors.py +40 -0
- elkpi_oxa-1.0.0/tests/test_smoke.py +12 -0
- elkpi_oxa-1.0.0/tests/test_sse.py +78 -0
- elkpi_oxa-1.0.0/tests/test_stream_unit.py +78 -0
- elkpi_oxa-1.0.0/tests/test_stream_vectors.py +64 -0
- 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
|
+
|
elkpi_oxa-1.0.0/PKG-INFO
ADDED
|
@@ -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
|
+
)
|