prism-ai-core 0.1.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.
- prism_ai_core-0.1.0/.gitignore +13 -0
- prism_ai_core-0.1.0/LICENSE +21 -0
- prism_ai_core-0.1.0/PKG-INFO +193 -0
- prism_ai_core-0.1.0/README.md +170 -0
- prism_ai_core-0.1.0/pyproject.toml +68 -0
- prism_ai_core-0.1.0/src/prism/__init__.py +181 -0
- prism_ai_core-0.1.0/src/prism/_php.py +90 -0
- prism_ai_core-0.1.0/src/prism/audio/__init__.py +15 -0
- prism_ai_core-0.1.0/src/prism/audio/pending_request.py +123 -0
- prism_ai_core-0.1.0/src/prism/audio/request.py +31 -0
- prism_ai_core-0.1.0/src/prism/audio/response.py +46 -0
- prism_ai_core-0.1.0/src/prism/batch/__init__.py +39 -0
- prism_ai_core-0.1.0/src/prism/batch/batch_job.py +164 -0
- prism_ai_core-0.1.0/src/prism/batch/pending_request.py +143 -0
- prism_ai_core-0.1.0/src/prism/batch/request.py +85 -0
- prism_ai_core-0.1.0/src/prism/canonical.py +70 -0
- prism_ai_core-0.1.0/src/prism/embeddings/__init__.py +9 -0
- prism_ai_core-0.1.0/src/prism/embeddings/pending_request.py +111 -0
- prism_ai_core-0.1.0/src/prism/embeddings/request.py +26 -0
- prism_ai_core-0.1.0/src/prism/embeddings/response.py +27 -0
- prism_ai_core-0.1.0/src/prism/enums.py +80 -0
- prism_ai_core-0.1.0/src/prism/errors.py +132 -0
- prism_ai_core-0.1.0/src/prism/files/__init__.py +25 -0
- prism_ai_core-0.1.0/src/prism/files/file_data.py +71 -0
- prism_ai_core-0.1.0/src/prism/files/pending_request.py +149 -0
- prism_ai_core-0.1.0/src/prism/files/request.py +76 -0
- prism_ai_core-0.1.0/src/prism/fim/__init__.py +9 -0
- prism_ai_core-0.1.0/src/prism/fim/pending_request.py +108 -0
- prism_ai_core-0.1.0/src/prism/fim/request.py +32 -0
- prism_ai_core-0.1.0/src/prism/fim/response.py +36 -0
- prism_ai_core-0.1.0/src/prism/http.py +273 -0
- prism_ai_core-0.1.0/src/prism/images/__init__.py +9 -0
- prism_ai_core-0.1.0/src/prism/images/pending_request.py +88 -0
- prism_ai_core-0.1.0/src/prism/images/request.py +17 -0
- prism_ai_core-0.1.0/src/prism/images/response.py +39 -0
- prism_ai_core-0.1.0/src/prism/moderation/__init__.py +9 -0
- prism_ai_core-0.1.0/src/prism/moderation/pending_request.py +85 -0
- prism_ai_core-0.1.0/src/prism/moderation/request.py +18 -0
- prism_ai_core-0.1.0/src/prism/moderation/response.py +40 -0
- prism_ai_core-0.1.0/src/prism/prism.py +70 -0
- prism_ai_core-0.1.0/src/prism/providers/__init__.py +12 -0
- prism_ai_core-0.1.0/src/prism/providers/anthropic/__init__.py +8 -0
- prism_ai_core-0.1.0/src/prism/providers/anthropic/maps.py +339 -0
- prism_ai_core-0.1.0/src/prism/providers/anthropic/provider.py +233 -0
- prism_ai_core-0.1.0/src/prism/providers/anthropic/rate_limits.py +68 -0
- prism_ai_core-0.1.0/src/prism/providers/anthropic/request_body.py +125 -0
- prism_ai_core-0.1.0/src/prism/providers/anthropic/response.py +166 -0
- prism_ai_core-0.1.0/src/prism/providers/anthropic/stream_events.py +197 -0
- prism_ai_core-0.1.0/src/prism/providers/base.py +82 -0
- prism_ai_core-0.1.0/src/prism/providers/mistral/__init__.py +8 -0
- prism_ai_core-0.1.0/src/prism/providers/mistral/embeddings.py +66 -0
- prism_ai_core-0.1.0/src/prism/providers/mistral/fim.py +65 -0
- prism_ai_core-0.1.0/src/prism/providers/mistral/maps.py +243 -0
- prism_ai_core-0.1.0/src/prism/providers/mistral/provider.py +291 -0
- prism_ai_core-0.1.0/src/prism/providers/mistral/rate_limits.py +68 -0
- prism_ai_core-0.1.0/src/prism/providers/mistral/request_body.py +112 -0
- prism_ai_core-0.1.0/src/prism/providers/mistral/response.py +227 -0
- prism_ai_core-0.1.0/src/prism/providers/mistral/stream_events.py +189 -0
- prism_ai_core-0.1.0/src/prism/providers/openai/__init__.py +9 -0
- prism_ai_core-0.1.0/src/prism/providers/openai/audio.py +145 -0
- prism_ai_core-0.1.0/src/prism/providers/openai/batch.py +297 -0
- prism_ai_core-0.1.0/src/prism/providers/openai/embeddings.py +90 -0
- prism_ai_core-0.1.0/src/prism/providers/openai/files.py +127 -0
- prism_ai_core-0.1.0/src/prism/providers/openai/images.py +90 -0
- prism_ai_core-0.1.0/src/prism/providers/openai/maps.py +362 -0
- prism_ai_core-0.1.0/src/prism/providers/openai/moderation.py +40 -0
- prism_ai_core-0.1.0/src/prism/providers/openai/provider.py +619 -0
- prism_ai_core-0.1.0/src/prism/providers/openai/rate_limits.py +85 -0
- prism_ai_core-0.1.0/src/prism/providers/openai/request_body.py +77 -0
- prism_ai_core-0.1.0/src/prism/providers/openai/response.py +233 -0
- prism_ai_core-0.1.0/src/prism/providers/openai/stream_events.py +126 -0
- prism_ai_core-0.1.0/src/prism/providers/openai/structured_body.py +64 -0
- prism_ai_core-0.1.0/src/prism/providers/openai/structured_mode.py +53 -0
- prism_ai_core-0.1.0/src/prism/providers/rate_limits.py +65 -0
- prism_ai_core-0.1.0/src/prism/providers/support.py +46 -0
- prism_ai_core-0.1.0/src/prism/py.typed +0 -0
- prism_ai_core-0.1.0/src/prism/registry.py +66 -0
- prism_ai_core-0.1.0/src/prism/schema.py +195 -0
- prism_ai_core-0.1.0/src/prism/streaming/__init__.py +31 -0
- prism_ai_core-0.1.0/src/prism/streaming/events.py +167 -0
- prism_ai_core-0.1.0/src/prism/streaming/sse.py +60 -0
- prism_ai_core-0.1.0/src/prism/structured/__init__.py +15 -0
- prism_ai_core-0.1.0/src/prism/structured/extract.py +53 -0
- prism_ai_core-0.1.0/src/prism/structured/from_text.py +33 -0
- prism_ai_core-0.1.0/src/prism/structured/pending_request.py +83 -0
- prism_ai_core-0.1.0/src/prism/structured/request.py +30 -0
- prism_ai_core-0.1.0/src/prism/structured/response.py +49 -0
- prism_ai_core-0.1.0/src/prism/text/__init__.py +11 -0
- prism_ai_core-0.1.0/src/prism/text/pending_request.py +249 -0
- prism_ai_core-0.1.0/src/prism/text/request.py +51 -0
- prism_ai_core-0.1.0/src/prism/text/response.py +47 -0
- prism_ai_core-0.1.0/src/prism/text/response_builder.py +82 -0
- prism_ai_core-0.1.0/src/prism/text/step.py +58 -0
- prism_ai_core-0.1.0/src/prism/tool.py +85 -0
- prism_ai_core-0.1.0/src/prism/value_objects/__init__.py +45 -0
- prism_ai_core-0.1.0/src/prism/value_objects/_opaque.py +24 -0
- prism_ai_core-0.1.0/src/prism/value_objects/embedding.py +62 -0
- prism_ai_core-0.1.0/src/prism/value_objects/generated_audio.py +33 -0
- prism_ai_core-0.1.0/src/prism/value_objects/generated_image.py +44 -0
- prism_ai_core-0.1.0/src/prism/value_objects/media.py +88 -0
- prism_ai_core-0.1.0/src/prism/value_objects/media_file.py +363 -0
- prism_ai_core-0.1.0/src/prism/value_objects/messages.py +212 -0
- prism_ai_core-0.1.0/src/prism/value_objects/meta.py +53 -0
- prism_ai_core-0.1.0/src/prism/value_objects/moderation_result.py +68 -0
- prism_ai_core-0.1.0/src/prism/value_objects/provider_rate_limit.py +142 -0
- prism_ai_core-0.1.0/src/prism/value_objects/provider_tool.py +33 -0
- prism_ai_core-0.1.0/src/prism/value_objects/provider_tool_call.py +36 -0
- prism_ai_core-0.1.0/src/prism/value_objects/tool_call.py +121 -0
- prism_ai_core-0.1.0/src/prism/value_objects/tool_result.py +55 -0
- prism_ai_core-0.1.0/src/prism/value_objects/usage.py +48 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 TJ MIller
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: prism-ai-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python port of Prism — a unified API layer over LLM providers.
|
|
5
|
+
Author: Particle Academy
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: ai,llm,openai,prism
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# prism-ai-core
|
|
25
|
+
|
|
26
|
+
A unified API layer over LLM providers — the Python port of
|
|
27
|
+
[Prism](https://github.com/Particle-Academy/prism).
|
|
28
|
+
|
|
29
|
+
Zero runtime dependencies. Python 3.10+.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from prism import Prism
|
|
33
|
+
|
|
34
|
+
response = (
|
|
35
|
+
Prism.text()
|
|
36
|
+
.using("openai", "gpt-4o")
|
|
37
|
+
.with_prompt("Who are you?")
|
|
38
|
+
.as_text()
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
print(response.text)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
> **Working on this package?** Read **[`AGENTS.md`](AGENTS.md)** first — the boundary
|
|
45
|
+
> this package has to hold, the gates that must be green, and the traps that have
|
|
46
|
+
> already caught someone.
|
|
47
|
+
> `@link AGENTS.md`
|
|
48
|
+
|
|
49
|
+
## Scope
|
|
50
|
+
|
|
51
|
+
**Every capability the reference has**, and three of its eighteen providers.
|
|
52
|
+
That is the shape of the remaining drift: one axis, not two.
|
|
53
|
+
|
|
54
|
+
- **Capabilities — all of them.** `text`, `structured`, `stream`, `embeddings`,
|
|
55
|
+
`images`, `moderation`, `audio` (`text_to_speech` and `speech_to_text`),
|
|
56
|
+
`files`, `batch`, `fim`. A user message carries images and documents, and each
|
|
57
|
+
provider spells them its own way.
|
|
58
|
+
- **Providers — `anthropic`, `mistral`, `openai`.** The OpenAI provider speaks
|
|
59
|
+
the **Responses API**; Mistral brings the chat-completions shape, which is a
|
|
60
|
+
different wire format rather than a variant of it.
|
|
61
|
+
|
|
62
|
+
Not in this port: **the tool-execution loop** — a response that finishes on tool
|
|
63
|
+
calls is refused with `tool_loop_not_supported` rather than half-executed — and
|
|
64
|
+
the fifteen providers the reference has and this one does not. An unsupported
|
|
65
|
+
capability raises a coded error rather than a missing attribute.
|
|
66
|
+
|
|
67
|
+
Not every capability reaches every provider, because not every provider has one.
|
|
68
|
+
`images`, `moderation`, `text_to_speech`, `files` and `batch` are OpenAI-only
|
|
69
|
+
here, and `fim` is Mistral-only because Mistral is the only provider that
|
|
70
|
+
implements it at all — in the reference as well. See the envelope's port gaps
|
|
71
|
+
register for which of those are work someone could do and which are facts about
|
|
72
|
+
the provider.
|
|
73
|
+
|
|
74
|
+
Two things this port has that the reference does not:
|
|
75
|
+
|
|
76
|
+
- **Every failure carries a stable `code`** (`prism.ErrorCode`). The reference
|
|
77
|
+
identifies failures by an English sentence, which forces consumers to match on
|
|
78
|
+
prose. The prose here is explicitly not part of the contract; the code is.
|
|
79
|
+
- **Every value object rebuilds.** `to_dict()` *and* `from_dict()`. The
|
|
80
|
+
reference can write its value objects and cannot read them back, a gap that
|
|
81
|
+
forced a downstream package to invent its own rehydration and ship a defect
|
|
82
|
+
with it.
|
|
83
|
+
|
|
84
|
+
## Install
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
pip install prism-ai-core
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The distribution is `prism-ai-core`; the package it installs is imported as
|
|
91
|
+
`prism`. To track `main` instead:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
pip install "git+https://github.com/Particle-Academy/prism-py"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Do not `pip install prism-ai`. That name on PyPI is somebody else's package**
|
|
98
|
+
— author `gwhite`, shipping a `prism_ai` module, unrelated to this project. It
|
|
99
|
+
is not a squat to route around later: it publishes **the same version number**
|
|
100
|
+
this port carries, so a pin of `prism-ai==0.1.0` resolves to the stranger's
|
|
101
|
+
distribution with nothing to tell you apart.
|
|
102
|
+
|
|
103
|
+
It is not inert either. It declares `httpx`, `requests` and `tqdm`, where this
|
|
104
|
+
port declares none, and installing it has been reported ROLLING HTTPX BACK
|
|
105
|
+
(0.28.1 → 0.25.2) and breaking an MCP client sharing the environment. pip prints
|
|
106
|
+
that conflict as a warning *after* the words "Successfully installed", so a CI
|
|
107
|
+
or otherwise non-interactive install reports success and the damage surfaces
|
|
108
|
+
somewhere else entirely.
|
|
109
|
+
|
|
110
|
+
Pin the git URL, and leave a comment where you pin it — this is the kind of line
|
|
111
|
+
a later tidy-up "corrects" into a version specifier.
|
|
112
|
+
|
|
113
|
+
Configuration comes from explicit constructor arguments, falling back to
|
|
114
|
+
`OPENAI_API_KEY`, `OPENAI_URL`, `OPENAI_ORGANIZATION` and `OPENAI_PROJECT`. The
|
|
115
|
+
HTTP transport is injectable, so nothing has to reach the network in a test:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from prism import OpenAI, Prism
|
|
119
|
+
|
|
120
|
+
pending = Prism.text().using("openai", "gpt-4o", {"transport": my_transport})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Parity
|
|
124
|
+
|
|
125
|
+
What this port must do is pinned by
|
|
126
|
+
[**prism-parity**](https://github.com/Particle-Academy/prism-parity), not by this
|
|
127
|
+
README. The corpus is the contract — the cases, the goldens, the per-language
|
|
128
|
+
skips and the discrimination probes all live there, each with its own notes
|
|
129
|
+
saying what it exists to catch. Restating them here would only give them a
|
|
130
|
+
second copy to drift from.
|
|
131
|
+
|
|
132
|
+
The corpus and its loader install as the `prism-conformance` package:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
git clone https://github.com/Particle-Academy/prism-parity .parity
|
|
136
|
+
pip install ./.parity/loaders/py
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Install it **last**: a path install can be pruned by a later install step. The
|
|
140
|
+
loader finds its fixtures by walking up from its own installed location, so
|
|
141
|
+
nothing here ever resolves a path into a sibling checkout.
|
|
142
|
+
|
|
143
|
+
Run every suite:
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
python conformance/runner.py
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Or one suite, or under a probe:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
python conformance/runner.py --suite openai-text-request
|
|
153
|
+
python conformance/runner.py --probe omit-null-keys
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
stdout is JSON and nothing else; the corpus version, digest and root go to
|
|
157
|
+
stderr on every run. Exit 0 when every case passed or skipped, 1 on a failure,
|
|
158
|
+
2 when the corpus failed to load, 3 when the runner could not start.
|
|
159
|
+
|
|
160
|
+
### Probes
|
|
161
|
+
|
|
162
|
+
`conformance/mutations.py` implements each probe the corpus declares as an
|
|
163
|
+
injected defect, and `tests/test_probes.py` asserts that every one fails
|
|
164
|
+
*exactly* the set of case ids the corpus names — not a superset, not "at least
|
|
165
|
+
one" — and that the faithful control fails nothing. That is what makes the
|
|
166
|
+
conformance table a measurement rather than decoration.
|
|
167
|
+
|
|
168
|
+
Nothing in `src/prism` knows any of this exists. A defect a port can switch on
|
|
169
|
+
is a defect a port can ship, so the mutants are installed from outside, over the
|
|
170
|
+
real library's output, and removed again.
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
`src/` layout, so install the package before running the tests:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
python -m venv .venv
|
|
178
|
+
.venv/Scripts/python -m pip install -e ".[dev]"
|
|
179
|
+
.venv/Scripts/python -m pip install ../prism-parity/loaders/py
|
|
180
|
+
|
|
181
|
+
python -m pytest
|
|
182
|
+
python -m mypy --strict
|
|
183
|
+
python -m ruff check .
|
|
184
|
+
python -m ruff format --check .
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Set `PRISM_CORPUS_ROOT` to run the conformance tests against a parity checkout
|
|
188
|
+
whose loader copy has not been re-synced yet. Unset — the normal case — the
|
|
189
|
+
loader discovers its own fixtures.
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# prism-ai-core
|
|
2
|
+
|
|
3
|
+
A unified API layer over LLM providers — the Python port of
|
|
4
|
+
[Prism](https://github.com/Particle-Academy/prism).
|
|
5
|
+
|
|
6
|
+
Zero runtime dependencies. Python 3.10+.
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from prism import Prism
|
|
10
|
+
|
|
11
|
+
response = (
|
|
12
|
+
Prism.text()
|
|
13
|
+
.using("openai", "gpt-4o")
|
|
14
|
+
.with_prompt("Who are you?")
|
|
15
|
+
.as_text()
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
print(response.text)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
> **Working on this package?** Read **[`AGENTS.md`](AGENTS.md)** first — the boundary
|
|
22
|
+
> this package has to hold, the gates that must be green, and the traps that have
|
|
23
|
+
> already caught someone.
|
|
24
|
+
> `@link AGENTS.md`
|
|
25
|
+
|
|
26
|
+
## Scope
|
|
27
|
+
|
|
28
|
+
**Every capability the reference has**, and three of its eighteen providers.
|
|
29
|
+
That is the shape of the remaining drift: one axis, not two.
|
|
30
|
+
|
|
31
|
+
- **Capabilities — all of them.** `text`, `structured`, `stream`, `embeddings`,
|
|
32
|
+
`images`, `moderation`, `audio` (`text_to_speech` and `speech_to_text`),
|
|
33
|
+
`files`, `batch`, `fim`. A user message carries images and documents, and each
|
|
34
|
+
provider spells them its own way.
|
|
35
|
+
- **Providers — `anthropic`, `mistral`, `openai`.** The OpenAI provider speaks
|
|
36
|
+
the **Responses API**; Mistral brings the chat-completions shape, which is a
|
|
37
|
+
different wire format rather than a variant of it.
|
|
38
|
+
|
|
39
|
+
Not in this port: **the tool-execution loop** — a response that finishes on tool
|
|
40
|
+
calls is refused with `tool_loop_not_supported` rather than half-executed — and
|
|
41
|
+
the fifteen providers the reference has and this one does not. An unsupported
|
|
42
|
+
capability raises a coded error rather than a missing attribute.
|
|
43
|
+
|
|
44
|
+
Not every capability reaches every provider, because not every provider has one.
|
|
45
|
+
`images`, `moderation`, `text_to_speech`, `files` and `batch` are OpenAI-only
|
|
46
|
+
here, and `fim` is Mistral-only because Mistral is the only provider that
|
|
47
|
+
implements it at all — in the reference as well. See the envelope's port gaps
|
|
48
|
+
register for which of those are work someone could do and which are facts about
|
|
49
|
+
the provider.
|
|
50
|
+
|
|
51
|
+
Two things this port has that the reference does not:
|
|
52
|
+
|
|
53
|
+
- **Every failure carries a stable `code`** (`prism.ErrorCode`). The reference
|
|
54
|
+
identifies failures by an English sentence, which forces consumers to match on
|
|
55
|
+
prose. The prose here is explicitly not part of the contract; the code is.
|
|
56
|
+
- **Every value object rebuilds.** `to_dict()` *and* `from_dict()`. The
|
|
57
|
+
reference can write its value objects and cannot read them back, a gap that
|
|
58
|
+
forced a downstream package to invent its own rehydration and ship a defect
|
|
59
|
+
with it.
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
pip install prism-ai-core
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The distribution is `prism-ai-core`; the package it installs is imported as
|
|
68
|
+
`prism`. To track `main` instead:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
pip install "git+https://github.com/Particle-Academy/prism-py"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Do not `pip install prism-ai`. That name on PyPI is somebody else's package**
|
|
75
|
+
— author `gwhite`, shipping a `prism_ai` module, unrelated to this project. It
|
|
76
|
+
is not a squat to route around later: it publishes **the same version number**
|
|
77
|
+
this port carries, so a pin of `prism-ai==0.1.0` resolves to the stranger's
|
|
78
|
+
distribution with nothing to tell you apart.
|
|
79
|
+
|
|
80
|
+
It is not inert either. It declares `httpx`, `requests` and `tqdm`, where this
|
|
81
|
+
port declares none, and installing it has been reported ROLLING HTTPX BACK
|
|
82
|
+
(0.28.1 → 0.25.2) and breaking an MCP client sharing the environment. pip prints
|
|
83
|
+
that conflict as a warning *after* the words "Successfully installed", so a CI
|
|
84
|
+
or otherwise non-interactive install reports success and the damage surfaces
|
|
85
|
+
somewhere else entirely.
|
|
86
|
+
|
|
87
|
+
Pin the git URL, and leave a comment where you pin it — this is the kind of line
|
|
88
|
+
a later tidy-up "corrects" into a version specifier.
|
|
89
|
+
|
|
90
|
+
Configuration comes from explicit constructor arguments, falling back to
|
|
91
|
+
`OPENAI_API_KEY`, `OPENAI_URL`, `OPENAI_ORGANIZATION` and `OPENAI_PROJECT`. The
|
|
92
|
+
HTTP transport is injectable, so nothing has to reach the network in a test:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from prism import OpenAI, Prism
|
|
96
|
+
|
|
97
|
+
pending = Prism.text().using("openai", "gpt-4o", {"transport": my_transport})
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Parity
|
|
101
|
+
|
|
102
|
+
What this port must do is pinned by
|
|
103
|
+
[**prism-parity**](https://github.com/Particle-Academy/prism-parity), not by this
|
|
104
|
+
README. The corpus is the contract — the cases, the goldens, the per-language
|
|
105
|
+
skips and the discrimination probes all live there, each with its own notes
|
|
106
|
+
saying what it exists to catch. Restating them here would only give them a
|
|
107
|
+
second copy to drift from.
|
|
108
|
+
|
|
109
|
+
The corpus and its loader install as the `prism-conformance` package:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
git clone https://github.com/Particle-Academy/prism-parity .parity
|
|
113
|
+
pip install ./.parity/loaders/py
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Install it **last**: a path install can be pruned by a later install step. The
|
|
117
|
+
loader finds its fixtures by walking up from its own installed location, so
|
|
118
|
+
nothing here ever resolves a path into a sibling checkout.
|
|
119
|
+
|
|
120
|
+
Run every suite:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
python conformance/runner.py
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Or one suite, or under a probe:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
python conformance/runner.py --suite openai-text-request
|
|
130
|
+
python conformance/runner.py --probe omit-null-keys
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
stdout is JSON and nothing else; the corpus version, digest and root go to
|
|
134
|
+
stderr on every run. Exit 0 when every case passed or skipped, 1 on a failure,
|
|
135
|
+
2 when the corpus failed to load, 3 when the runner could not start.
|
|
136
|
+
|
|
137
|
+
### Probes
|
|
138
|
+
|
|
139
|
+
`conformance/mutations.py` implements each probe the corpus declares as an
|
|
140
|
+
injected defect, and `tests/test_probes.py` asserts that every one fails
|
|
141
|
+
*exactly* the set of case ids the corpus names — not a superset, not "at least
|
|
142
|
+
one" — and that the faithful control fails nothing. That is what makes the
|
|
143
|
+
conformance table a measurement rather than decoration.
|
|
144
|
+
|
|
145
|
+
Nothing in `src/prism` knows any of this exists. A defect a port can switch on
|
|
146
|
+
is a defect a port can ship, so the mutants are installed from outside, over the
|
|
147
|
+
real library's output, and removed again.
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
`src/` layout, so install the package before running the tests:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
python -m venv .venv
|
|
155
|
+
.venv/Scripts/python -m pip install -e ".[dev]"
|
|
156
|
+
.venv/Scripts/python -m pip install ../prism-parity/loaders/py
|
|
157
|
+
|
|
158
|
+
python -m pytest
|
|
159
|
+
python -m mypy --strict
|
|
160
|
+
python -m ruff check .
|
|
161
|
+
python -m ruff format --check .
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Set `PRISM_CORPUS_ROOT` to run the conformance tests against a parity checkout
|
|
165
|
+
whose loader copy has not been re-synced yet. Unset — the normal case — the
|
|
166
|
+
loader discovers its own fixtures.
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "prism-ai-core"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python port of Prism — a unified API layer over LLM providers."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "Particle Academy" }]
|
|
14
|
+
keywords = ["llm", "openai", "prism", "ai"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
dependencies = []
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = ["pytest>=8.0", "mypy>=1.11", "ruff>=0.6"]
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["src/prism"]
|
|
32
|
+
|
|
33
|
+
# The source archive carries the package and its licence, and nothing else: not the Lab's
|
|
34
|
+
# loopback agent, CI workflows or contributor notes. Anchored with a leading slash, since an
|
|
35
|
+
# unanchored README.md also matches agent/README.md.
|
|
36
|
+
[tool.hatch.build.targets.sdist]
|
|
37
|
+
include = ["/src/prism", "/README.md", "/LICENSE", "/pyproject.toml"]
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
line-length = 100
|
|
41
|
+
target-version = "py310"
|
|
42
|
+
src = ["src", "conformance", "tests"]
|
|
43
|
+
# Prose, not source. Ruff would collapse the README's chained-builder examples
|
|
44
|
+
# onto one line, which is exactly the shape they exist to show.
|
|
45
|
+
extend-exclude = ["*.md"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
select = ["E", "F", "W", "I", "B", "UP", "N", "RUF"]
|
|
49
|
+
ignore = [
|
|
50
|
+
# `Tool.format` / schema `format` mirror the reference field names.
|
|
51
|
+
"A003",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[tool.ruff.lint.per-file-ignores]
|
|
55
|
+
"tests/*" = ["E501"]
|
|
56
|
+
|
|
57
|
+
[tool.mypy]
|
|
58
|
+
python_version = "3.10"
|
|
59
|
+
strict = true
|
|
60
|
+
files = ["src", "conformance", "tests"]
|
|
61
|
+
|
|
62
|
+
[tool.pytest.ini_options]
|
|
63
|
+
testpaths = ["tests"]
|
|
64
|
+
# `-ra` so skips and their reasons are always printed. A skip nobody sees is a
|
|
65
|
+
# suite that quietly shrank.
|
|
66
|
+
addopts = "-q -ra"
|
|
67
|
+
# The conformance runner is test support outside src/, and the tests import it.
|
|
68
|
+
pythonpath = ["."]
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""prism — a unified API layer over LLM providers.
|
|
2
|
+
|
|
3
|
+
This is the Python port of Prism:
|
|
4
|
+
|
|
5
|
+
>>> from prism import Prism
|
|
6
|
+
>>> response = ( # doctest: +SKIP
|
|
7
|
+
... Prism.text()
|
|
8
|
+
... .using("openai", "gpt-4o")
|
|
9
|
+
... .with_prompt("Who are you?")
|
|
10
|
+
... .as_text()
|
|
11
|
+
... )
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from prism import canonical
|
|
17
|
+
from prism.audio import (
|
|
18
|
+
AudioPendingRequest,
|
|
19
|
+
AudioResponse,
|
|
20
|
+
AudioTextResponse,
|
|
21
|
+
SpeechToTextRequest,
|
|
22
|
+
TextToSpeechRequest,
|
|
23
|
+
)
|
|
24
|
+
from prism.batch import (
|
|
25
|
+
BatchJob,
|
|
26
|
+
BatchJobError,
|
|
27
|
+
BatchJobRequestCounts,
|
|
28
|
+
BatchListResult,
|
|
29
|
+
BatchPendingRequest,
|
|
30
|
+
BatchRequest,
|
|
31
|
+
BatchRequestItem,
|
|
32
|
+
BatchResultItem,
|
|
33
|
+
BatchResultStatus,
|
|
34
|
+
BatchStatus,
|
|
35
|
+
CancelBatchRequest,
|
|
36
|
+
GetBatchResultsRequest,
|
|
37
|
+
ListBatchesRequest,
|
|
38
|
+
RetrieveBatchRequest,
|
|
39
|
+
)
|
|
40
|
+
from prism.canonical import encode as canonical_encode
|
|
41
|
+
from prism.enums import FinishReason, ToolChoice
|
|
42
|
+
from prism.enums import Provider as ProviderName
|
|
43
|
+
from prism.errors import ErrorCode, PrismError
|
|
44
|
+
from prism.files import (
|
|
45
|
+
DeleteFileRequest,
|
|
46
|
+
DeleteFileResult,
|
|
47
|
+
DownloadFileRequest,
|
|
48
|
+
FileData,
|
|
49
|
+
FileListResult,
|
|
50
|
+
FilesPendingRequest,
|
|
51
|
+
GetFileMetadataRequest,
|
|
52
|
+
ListFilesRequest,
|
|
53
|
+
UploadFileRequest,
|
|
54
|
+
)
|
|
55
|
+
from prism.fim import FimPendingRequest, FimRequest, FimResponse
|
|
56
|
+
from prism.http import (
|
|
57
|
+
HttpRequest,
|
|
58
|
+
HttpResponse,
|
|
59
|
+
MultipartBody,
|
|
60
|
+
MultipartFile,
|
|
61
|
+
Transport,
|
|
62
|
+
UrllibTransport,
|
|
63
|
+
encode_multipart,
|
|
64
|
+
)
|
|
65
|
+
from prism.prism import Prism
|
|
66
|
+
from prism.providers.base import Provider
|
|
67
|
+
from prism.providers.mistral import Mistral, MistralStreamMapper
|
|
68
|
+
from prism.providers.openai import OpenAI, build_request_body, build_tools, parse_text_response
|
|
69
|
+
from prism.registry import register_provider, resolve_provider
|
|
70
|
+
from prism.schema import BooleanSchema, NumberSchema, Schema, StringSchema
|
|
71
|
+
from prism.text import PendingRequest, Request, Response, ResponseBuilder, Step
|
|
72
|
+
from prism.tool import Tool
|
|
73
|
+
from prism.value_objects import (
|
|
74
|
+
AssistantMessage,
|
|
75
|
+
Message,
|
|
76
|
+
Meta,
|
|
77
|
+
ProviderRateLimit,
|
|
78
|
+
ProviderTool,
|
|
79
|
+
ProviderToolCall,
|
|
80
|
+
SystemMessage,
|
|
81
|
+
Text,
|
|
82
|
+
ToolCall,
|
|
83
|
+
ToolResult,
|
|
84
|
+
ToolResultMessage,
|
|
85
|
+
Usage,
|
|
86
|
+
UserMessage,
|
|
87
|
+
message_from_dict,
|
|
88
|
+
)
|
|
89
|
+
from prism.value_objects.generated_audio import GeneratedAudio
|
|
90
|
+
from prism.value_objects.media_file import Audio, Document, Image, Media, Video
|
|
91
|
+
|
|
92
|
+
__version__ = "0.1.0"
|
|
93
|
+
|
|
94
|
+
__all__ = [
|
|
95
|
+
"AssistantMessage",
|
|
96
|
+
"Audio",
|
|
97
|
+
"AudioPendingRequest",
|
|
98
|
+
"AudioResponse",
|
|
99
|
+
"AudioTextResponse",
|
|
100
|
+
"BatchJob",
|
|
101
|
+
"BatchJobError",
|
|
102
|
+
"BatchJobRequestCounts",
|
|
103
|
+
"BatchListResult",
|
|
104
|
+
"BatchPendingRequest",
|
|
105
|
+
"BatchRequest",
|
|
106
|
+
"BatchRequestItem",
|
|
107
|
+
"BatchResultItem",
|
|
108
|
+
"BatchResultStatus",
|
|
109
|
+
"BatchStatus",
|
|
110
|
+
"BooleanSchema",
|
|
111
|
+
"CancelBatchRequest",
|
|
112
|
+
"DeleteFileRequest",
|
|
113
|
+
"DeleteFileResult",
|
|
114
|
+
"Document",
|
|
115
|
+
"DownloadFileRequest",
|
|
116
|
+
"ErrorCode",
|
|
117
|
+
"FileData",
|
|
118
|
+
"FileListResult",
|
|
119
|
+
"FilesPendingRequest",
|
|
120
|
+
"FimPendingRequest",
|
|
121
|
+
"FimRequest",
|
|
122
|
+
"FimResponse",
|
|
123
|
+
"FinishReason",
|
|
124
|
+
"GeneratedAudio",
|
|
125
|
+
"GetBatchResultsRequest",
|
|
126
|
+
"GetFileMetadataRequest",
|
|
127
|
+
"HttpRequest",
|
|
128
|
+
"HttpResponse",
|
|
129
|
+
"Image",
|
|
130
|
+
"ListBatchesRequest",
|
|
131
|
+
"ListFilesRequest",
|
|
132
|
+
"Media",
|
|
133
|
+
"Message",
|
|
134
|
+
"Meta",
|
|
135
|
+
"Mistral",
|
|
136
|
+
"MistralStreamMapper",
|
|
137
|
+
"MultipartBody",
|
|
138
|
+
"MultipartFile",
|
|
139
|
+
"NumberSchema",
|
|
140
|
+
"OpenAI",
|
|
141
|
+
"PendingRequest",
|
|
142
|
+
"Prism",
|
|
143
|
+
"PrismError",
|
|
144
|
+
"Provider",
|
|
145
|
+
"ProviderName",
|
|
146
|
+
"ProviderRateLimit",
|
|
147
|
+
"ProviderTool",
|
|
148
|
+
"ProviderToolCall",
|
|
149
|
+
"Request",
|
|
150
|
+
"Response",
|
|
151
|
+
"ResponseBuilder",
|
|
152
|
+
"RetrieveBatchRequest",
|
|
153
|
+
"Schema",
|
|
154
|
+
"SpeechToTextRequest",
|
|
155
|
+
"Step",
|
|
156
|
+
"StringSchema",
|
|
157
|
+
"SystemMessage",
|
|
158
|
+
"Text",
|
|
159
|
+
"TextToSpeechRequest",
|
|
160
|
+
"Tool",
|
|
161
|
+
"ToolCall",
|
|
162
|
+
"ToolChoice",
|
|
163
|
+
"ToolResult",
|
|
164
|
+
"ToolResultMessage",
|
|
165
|
+
"Transport",
|
|
166
|
+
"UploadFileRequest",
|
|
167
|
+
"UrllibTransport",
|
|
168
|
+
"Usage",
|
|
169
|
+
"UserMessage",
|
|
170
|
+
"Video",
|
|
171
|
+
"__version__",
|
|
172
|
+
"build_request_body",
|
|
173
|
+
"build_tools",
|
|
174
|
+
"canonical",
|
|
175
|
+
"canonical_encode",
|
|
176
|
+
"encode_multipart",
|
|
177
|
+
"message_from_dict",
|
|
178
|
+
"parse_text_response",
|
|
179
|
+
"register_provider",
|
|
180
|
+
"resolve_provider",
|
|
181
|
+
]
|