llm-conduit 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.
Files changed (76) hide show
  1. llm_conduit-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +71 -0
  2. llm_conduit-0.1.0/.github/ISSUE_TEMPLATE/config.yml +9 -0
  3. llm_conduit-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +55 -0
  4. llm_conduit-0.1.0/.github/ISSUE_TEMPLATE/new_provider.yml +51 -0
  5. llm_conduit-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +31 -0
  6. llm_conduit-0.1.0/.github/workflows/ci.yml +240 -0
  7. llm_conduit-0.1.0/.github/workflows/codeql.yml +35 -0
  8. llm_conduit-0.1.0/.github/workflows/publish.yml +192 -0
  9. llm_conduit-0.1.0/.gitignore +54 -0
  10. llm_conduit-0.1.0/CHANGELOG.md +48 -0
  11. llm_conduit-0.1.0/CONTRIBUTING.md +104 -0
  12. llm_conduit-0.1.0/PKG-INFO +894 -0
  13. llm_conduit-0.1.0/README.md +862 -0
  14. llm_conduit-0.1.0/conduit_sdk/__init__.py +40 -0
  15. llm_conduit-0.1.0/conduit_sdk/clients/__init__.py +6 -0
  16. llm_conduit-0.1.0/conduit_sdk/clients/embedding.py +85 -0
  17. llm_conduit-0.1.0/conduit_sdk/clients/image.py +81 -0
  18. llm_conduit-0.1.0/conduit_sdk/clients/llm.py +147 -0
  19. llm_conduit-0.1.0/conduit_sdk/clients/video.py +85 -0
  20. llm_conduit-0.1.0/conduit_sdk/core/__init__.py +32 -0
  21. llm_conduit-0.1.0/conduit_sdk/core/base.py +145 -0
  22. llm_conduit-0.1.0/conduit_sdk/core/config.py +104 -0
  23. llm_conduit-0.1.0/conduit_sdk/core/exceptions.py +119 -0
  24. llm_conduit-0.1.0/conduit_sdk/core/middleware.py +129 -0
  25. llm_conduit-0.1.0/conduit_sdk/core/protocols.py +93 -0
  26. llm_conduit-0.1.0/conduit_sdk/models/__init__.py +33 -0
  27. llm_conduit-0.1.0/conduit_sdk/models/common.py +94 -0
  28. llm_conduit-0.1.0/conduit_sdk/models/requests.py +197 -0
  29. llm_conduit-0.1.0/conduit_sdk/models/responses.py +202 -0
  30. llm_conduit-0.1.0/conduit_sdk/providers/__init__.py +14 -0
  31. llm_conduit-0.1.0/conduit_sdk/providers/anthropic/__init__.py +32 -0
  32. llm_conduit-0.1.0/conduit_sdk/providers/anthropic/llm.py +251 -0
  33. llm_conduit-0.1.0/conduit_sdk/providers/openai/__init__.py +33 -0
  34. llm_conduit-0.1.0/conduit_sdk/providers/openai/embedding.py +82 -0
  35. llm_conduit-0.1.0/conduit_sdk/providers/openai/image.py +111 -0
  36. llm_conduit-0.1.0/conduit_sdk/providers/openai/llm.py +211 -0
  37. llm_conduit-0.1.0/conduit_sdk/registry/__init__.py +4 -0
  38. llm_conduit-0.1.0/conduit_sdk/registry/model_registry.py +189 -0
  39. llm_conduit-0.1.0/conduit_sdk/registry/provider_registry.py +170 -0
  40. llm_conduit-0.1.0/conduit_sdk/utils/__init__.py +14 -0
  41. llm_conduit-0.1.0/conduit_sdk/utils/cost.py +165 -0
  42. llm_conduit-0.1.0/conduit_sdk/utils/logging.py +104 -0
  43. llm_conduit-0.1.0/conduit_sdk/utils/rate_limit.py +98 -0
  44. llm_conduit-0.1.0/conduit_sdk/utils/retry.py +70 -0
  45. llm_conduit-0.1.0/docs/architecture.svg +204 -0
  46. llm_conduit-0.1.0/examples/01_basic_llm.py +118 -0
  47. llm_conduit-0.1.0/examples/02_streaming.py +97 -0
  48. llm_conduit-0.1.0/examples/03_image_generation.py +108 -0
  49. llm_conduit-0.1.0/examples/04_video_generation.py +137 -0
  50. llm_conduit-0.1.0/examples/05_embeddings.py +139 -0
  51. llm_conduit-0.1.0/examples/06_cost_tracking.py +181 -0
  52. llm_conduit-0.1.0/examples/07_custom_middleware.py +199 -0
  53. llm_conduit-0.1.0/examples/08_registry.py +222 -0
  54. llm_conduit-0.1.0/examples/09_multimodal_pipeline.py +267 -0
  55. llm_conduit-0.1.0/examples/README.md +27 -0
  56. llm_conduit-0.1.0/examples/providers/__init__.py +1 -0
  57. llm_conduit-0.1.0/examples/providers/anthropic_client.py +107 -0
  58. llm_conduit-0.1.0/examples/providers/openai_client.py +173 -0
  59. llm_conduit-0.1.0/examples/providers/replicate_client.py +71 -0
  60. llm_conduit-0.1.0/examples/providers/runway_client.py +109 -0
  61. llm_conduit-0.1.0/pyproject.toml +78 -0
  62. llm_conduit-0.1.0/tests/__init__.py +0 -0
  63. llm_conduit-0.1.0/tests/conftest.py +152 -0
  64. llm_conduit-0.1.0/tests/providers/__init__.py +0 -0
  65. llm_conduit-0.1.0/tests/providers/conftest.py +253 -0
  66. llm_conduit-0.1.0/tests/providers/test_anthropic_llm.py +656 -0
  67. llm_conduit-0.1.0/tests/providers/test_openai_embedding.py +190 -0
  68. llm_conduit-0.1.0/tests/providers/test_openai_image.py +210 -0
  69. llm_conduit-0.1.0/tests/providers/test_openai_llm.py +394 -0
  70. llm_conduit-0.1.0/tests/test_embedding.py +45 -0
  71. llm_conduit-0.1.0/tests/test_image.py +54 -0
  72. llm_conduit-0.1.0/tests/test_llm.py +69 -0
  73. llm_conduit-0.1.0/tests/test_middleware.py +249 -0
  74. llm_conduit-0.1.0/tests/test_protocols.py +33 -0
  75. llm_conduit-0.1.0/tests/test_registry.py +140 -0
  76. llm_conduit-0.1.0/tests/test_video.py +37 -0
@@ -0,0 +1,71 @@
1
+ name: Bug Report
2
+ description: Report a bug or unexpected behaviour
3
+ labels: [bug, needs-triage]
4
+ assignees: []
5
+
6
+ body:
7
+ - type: markdown
8
+ attributes:
9
+ value: |
10
+ Thanks for taking the time to report a bug. Please fill out the sections below.
11
+
12
+ - type: input
13
+ id: version
14
+ attributes:
15
+ label: llm-conduit version
16
+ placeholder: "e.g. 0.1.0"
17
+ validations:
18
+ required: true
19
+
20
+ - type: input
21
+ id: python
22
+ attributes:
23
+ label: Python version
24
+ placeholder: "e.g. 3.11.8"
25
+ validations:
26
+ required: true
27
+
28
+ - type: dropdown
29
+ id: modality
30
+ attributes:
31
+ label: Affected modality
32
+ multiple: true
33
+ options:
34
+ - LLMClient
35
+ - ImageGenClient
36
+ - VideoGenClient
37
+ - EmbeddingClient
38
+ - Middleware (retry / rate-limit / cost / logging)
39
+ - Registry (ModelRegistry / ProviderRegistry)
40
+ - Other
41
+ validations:
42
+ required: true
43
+
44
+ - type: textarea
45
+ id: description
46
+ attributes:
47
+ label: Description
48
+ description: Clear description of the bug and what you expected to happen.
49
+ validations:
50
+ required: true
51
+
52
+ - type: textarea
53
+ id: repro
54
+ attributes:
55
+ label: Minimal reproduction
56
+ description: Smallest possible code snippet that reproduces the issue.
57
+ render: python
58
+ validations:
59
+ required: true
60
+
61
+ - type: textarea
62
+ id: traceback
63
+ attributes:
64
+ label: Full traceback / error output
65
+ render: text
66
+
67
+ - type: textarea
68
+ id: context
69
+ attributes:
70
+ label: Additional context
71
+ description: Provider name, OS, any other relevant details.
@@ -0,0 +1,9 @@
1
+ blank_issues_enabled: false
2
+
3
+ contact_links:
4
+ - name: Documentation
5
+ url: https://github.com/your-org/llm-conduit#readme
6
+ about: Read the README and examples/ before opening an issue.
7
+ - name: Discussions
8
+ url: https://github.com/your-org/llm-conduit/discussions
9
+ about: Ask questions and share ideas in Discussions.
@@ -0,0 +1,55 @@
1
+ name: Feature Request
2
+ description: Suggest a new feature or improvement
3
+ labels: [enhancement]
4
+ assignees: []
5
+
6
+ body:
7
+ - type: markdown
8
+ attributes:
9
+ value: |
10
+ Before opening a request, please check that no existing issue covers this.
11
+
12
+ - type: dropdown
13
+ id: area
14
+ attributes:
15
+ label: Area
16
+ options:
17
+ - New modality client
18
+ - New middleware
19
+ - New provider adapter (examples/)
20
+ - Registry / config
21
+ - Request / response models
22
+ - Documentation / examples
23
+ - CI / tooling
24
+ - Other
25
+ validations:
26
+ required: true
27
+
28
+ - type: textarea
29
+ id: problem
30
+ attributes:
31
+ label: Problem / motivation
32
+ description: What problem are you trying to solve? Why does the current SDK not cover it?
33
+ validations:
34
+ required: true
35
+
36
+ - type: textarea
37
+ id: proposal
38
+ attributes:
39
+ label: Proposed solution
40
+ description: How should this work? Include API sketches or pseudocode if helpful.
41
+ validations:
42
+ required: true
43
+
44
+ - type: textarea
45
+ id: alternatives
46
+ attributes:
47
+ label: Alternatives considered
48
+ description: What else did you consider, and why did you rule it out?
49
+
50
+ - type: checkboxes
51
+ id: contribution
52
+ attributes:
53
+ label: Contribution
54
+ options:
55
+ - label: I am willing to open a PR for this feature.
@@ -0,0 +1,51 @@
1
+ name: New Provider Adapter
2
+ description: Request or track a new provider adapter in examples/providers/
3
+ labels: [provider, enhancement]
4
+ assignees: []
5
+
6
+ body:
7
+ - type: input
8
+ id: provider
9
+ attributes:
10
+ label: Provider name
11
+ placeholder: "e.g. Stability AI, Mistral, Kling, ElevenLabs"
12
+ validations:
13
+ required: true
14
+
15
+ - type: checkboxes
16
+ id: modalities
17
+ attributes:
18
+ label: Modalities to support
19
+ options:
20
+ - label: LLM (chat completion + streaming)
21
+ - label: Image generation
22
+ - label: Video generation
23
+ - label: Embeddings
24
+ validations:
25
+ required: true
26
+
27
+ - type: input
28
+ id: sdk
29
+ attributes:
30
+ label: Provider's official Python SDK (if any)
31
+ placeholder: "e.g. pip install stability-sdk"
32
+
33
+ - type: input
34
+ id: docs
35
+ attributes:
36
+ label: API documentation URL
37
+ validations:
38
+ required: true
39
+
40
+ - type: textarea
41
+ id: notes
42
+ attributes:
43
+ label: Notes / quirks
44
+ description: Anything unusual about this provider's API (auth, async jobs, rate limits, etc.)
45
+
46
+ - type: checkboxes
47
+ id: contribution
48
+ attributes:
49
+ label: Contribution
50
+ options:
51
+ - label: I am willing to open a PR with this adapter.
@@ -0,0 +1,31 @@
1
+ ## Summary
2
+
3
+ <!-- What does this PR do? 1–3 sentences. -->
4
+
5
+ ## Type of change
6
+
7
+ - [ ] Bug fix
8
+ - [ ] New feature (new modality, middleware, registry capability)
9
+ - [ ] New provider adapter (in `examples/providers/`)
10
+ - [ ] Documentation / examples
11
+ - [ ] Refactor / internal improvement
12
+ - [ ] CI / tooling
13
+
14
+ ## Checklist
15
+
16
+ - [ ] `pytest tests/` passes locally
17
+ - [ ] `ruff check conduit_sdk tests examples` passes
18
+ - [ ] New code has corresponding tests
19
+ - [ ] Docstrings updated on any changed public API
20
+ - [ ] `CHANGELOG.md` updated (for non-trivial changes)
21
+
22
+ ## Breaking changes
23
+
24
+ <!-- Does this change the public interface of BaseClient, any abstract client, or any Pydantic model?
25
+ If yes, describe what breaks and why it is necessary. -->
26
+
27
+ None / describe here.
28
+
29
+ ## Related issues
30
+
31
+ Closes #
@@ -0,0 +1,240 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, develop]
6
+ pull_request:
7
+ branches: [main, develop]
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ # ──────────────────────────────────────────────────────────────
15
+ # Lint & format check
16
+ # ──────────────────────────────────────────────────────────────
17
+ lint:
18
+ name: Lint (ruff)
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - uses: astral-sh/setup-uv@v5
24
+ with:
25
+ version: "latest"
26
+
27
+ - name: Set up Python
28
+ uses: actions/setup-python@v5
29
+ with:
30
+ python-version: "3.11"
31
+
32
+ - name: Install dependencies
33
+ run: uv pip install --system ruff
34
+
35
+ - name: Run ruff (lint)
36
+ run: ruff check conduit_sdk tests examples
37
+
38
+ - name: Run ruff (format check)
39
+ run: ruff format --check conduit_sdk tests examples
40
+
41
+ # ──────────────────────────────────────────────────────────────
42
+ # Type checking
43
+ # ──────────────────────────────────────────────────────────────
44
+ typecheck:
45
+ name: Type check (mypy)
46
+ runs-on: ubuntu-latest
47
+ steps:
48
+ - uses: actions/checkout@v4
49
+
50
+ - uses: astral-sh/setup-uv@v5
51
+ with:
52
+ version: "latest"
53
+
54
+ - name: Set up Python
55
+ uses: actions/setup-python@v5
56
+ with:
57
+ python-version: "3.11"
58
+
59
+ - name: Install project + all extras + dev deps
60
+ run: uv pip install --system -e ".[dev,openai,anthropic]"
61
+
62
+ - name: Run mypy
63
+ run: mypy conduit_sdk
64
+
65
+ # ──────────────────────────────────────────────────────────────
66
+ # Tests — matrix across Python 3.10, 3.11, 3.12
67
+ # ──────────────────────────────────────────────────────────────
68
+ test:
69
+ name: Tests (Python ${{ matrix.python-version }})
70
+ runs-on: ubuntu-latest
71
+ strategy:
72
+ fail-fast: false
73
+ matrix:
74
+ python-version: ["3.10", "3.11", "3.12"]
75
+
76
+ steps:
77
+ - uses: actions/checkout@v4
78
+
79
+ - uses: astral-sh/setup-uv@v5
80
+ with:
81
+ version: "latest"
82
+
83
+ - name: Set up Python ${{ matrix.python-version }}
84
+ uses: actions/setup-python@v5
85
+ with:
86
+ python-version: ${{ matrix.python-version }}
87
+
88
+ - name: Install project + all extras + dev deps
89
+ run: uv pip install --system -e ".[dev,openai,anthropic]"
90
+
91
+ - name: Run pytest with coverage
92
+ run: |
93
+ pytest tests/ \
94
+ --tb=short \
95
+ --cov=conduit_sdk \
96
+ --cov-report=term-missing \
97
+ --cov-report=xml \
98
+ --cov-fail-under=80
99
+
100
+ - name: Upload coverage report
101
+ if: matrix.python-version == '3.11'
102
+ uses: actions/upload-artifact@v4
103
+ with:
104
+ name: coverage-report
105
+ path: coverage.xml
106
+ retention-days: 7
107
+
108
+ # ──────────────────────────────────────────────────────────────
109
+ # Run example scripts (smoke test — no API keys needed)
110
+ # ──────────────────────────────────────────────────────────────
111
+ examples:
112
+ name: Smoke-test examples
113
+ runs-on: ubuntu-latest
114
+ needs: test
115
+
116
+ steps:
117
+ - uses: actions/checkout@v4
118
+
119
+ - uses: astral-sh/setup-uv@v5
120
+ with:
121
+ version: "latest"
122
+
123
+ - name: Set up Python
124
+ uses: actions/setup-python@v5
125
+ with:
126
+ python-version: "3.11"
127
+
128
+ - name: Install project + all extras
129
+ run: uv pip install --system -e ".[dev,openai,anthropic]"
130
+
131
+ - name: Run examples
132
+ run: |
133
+ for script in \
134
+ examples/01_basic_llm.py \
135
+ examples/02_streaming.py \
136
+ examples/03_image_generation.py \
137
+ examples/04_video_generation.py \
138
+ examples/05_embeddings.py \
139
+ examples/06_cost_tracking.py \
140
+ examples/07_custom_middleware.py \
141
+ examples/08_registry.py \
142
+ examples/09_multimodal_pipeline.py; do
143
+ echo "▶ Running $script"
144
+ python "$script"
145
+ echo "✓ $script passed"
146
+ done
147
+
148
+ # ──────────────────────────────────────────────────────────────
149
+ # Integration tests — real OpenAI API calls
150
+ # Only runs on push to main (not on every PR) to avoid burning credits.
151
+ # Requires OPENAI_API_KEY to be set as a GitHub repository secret.
152
+ # ──────────────────────────────────────────────────────────────
153
+ integration-openai:
154
+ name: Integration tests (OpenAI)
155
+ runs-on: ubuntu-latest
156
+ needs: test
157
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
158
+
159
+ steps:
160
+ - uses: actions/checkout@v4
161
+
162
+ - uses: astral-sh/setup-uv@v5
163
+ with:
164
+ version: "latest"
165
+
166
+ - name: Set up Python
167
+ uses: actions/setup-python@v5
168
+ with:
169
+ python-version: "3.11"
170
+
171
+ - name: Install project with OpenAI extra
172
+ run: uv pip install --system -e ".[dev,openai]"
173
+
174
+ - name: Run OpenAI integration tests
175
+ env:
176
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
177
+ run: |
178
+ pytest tests/providers/test_openai_llm.py \
179
+ tests/providers/test_openai_embedding.py \
180
+ tests/providers/test_openai_image.py \
181
+ -m integration \
182
+ -v \
183
+ --tb=short \
184
+ --timeout=60
185
+
186
+ # ──────────────────────────────────────────────────────────────
187
+ # Integration tests — real Anthropic API calls
188
+ # Only runs on push to main (not on every PR) to avoid burning credits.
189
+ # Requires ANTHROPIC_API_KEY to be set as a GitHub repository secret.
190
+ # ──────────────────────────────────────────────────────────────
191
+ integration-anthropic:
192
+ name: Integration tests (Anthropic)
193
+ runs-on: ubuntu-latest
194
+ needs: test
195
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
196
+
197
+ steps:
198
+ - uses: actions/checkout@v4
199
+
200
+ - uses: astral-sh/setup-uv@v5
201
+ with:
202
+ version: "latest"
203
+
204
+ - name: Set up Python
205
+ uses: actions/setup-python@v5
206
+ with:
207
+ python-version: "3.11"
208
+
209
+ - name: Install project with Anthropic extra
210
+ run: uv pip install --system -e ".[dev,anthropic]"
211
+
212
+ - name: Run Anthropic integration tests
213
+ env:
214
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
215
+ run: |
216
+ pytest tests/providers/test_anthropic_llm.py \
217
+ -m integration \
218
+ -v \
219
+ --tb=short \
220
+ --timeout=60
221
+
222
+ # ──────────────────────────────────────────────────────────────
223
+ # All checks must pass before merge (used as branch protection rule)
224
+ # ──────────────────────────────────────────────────────────────
225
+ ci-pass:
226
+ name: CI passed
227
+ runs-on: ubuntu-latest
228
+ needs: [lint, typecheck, test, examples]
229
+ if: always()
230
+ steps:
231
+ - name: Check all jobs
232
+ run: |
233
+ if [[ "${{ needs.lint.result }}" != "success" || \
234
+ "${{ needs.typecheck.result }}" != "success" || \
235
+ "${{ needs.test.result }}" != "success" || \
236
+ "${{ needs.examples.result }}" != "success" ]]; then
237
+ echo "One or more CI jobs failed."
238
+ exit 1
239
+ fi
240
+ echo "All CI jobs passed."
@@ -0,0 +1,35 @@
1
+ name: CodeQL Security Scan
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+ schedule:
9
+ - cron: "30 5 * * 1" # every Monday at 05:30 UTC
10
+
11
+ jobs:
12
+ analyze:
13
+ name: Analyze (Python)
14
+ runs-on: ubuntu-latest
15
+ permissions:
16
+ security-events: write
17
+ actions: read
18
+ contents: read
19
+
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - name: Initialize CodeQL
24
+ uses: github/codeql-action/init@v3
25
+ with:
26
+ languages: python
27
+ queries: security-and-quality
28
+
29
+ - name: Autobuild
30
+ uses: github/codeql-action/autobuild@v3
31
+
32
+ - name: Perform CodeQL Analysis
33
+ uses: github/codeql-action/analyze@v3
34
+ with:
35
+ category: "/language:python"
@@ -0,0 +1,192 @@
1
+ name: Publish to PyPI
2
+
3
+ # ──────────────────────────────────────────────────────────────
4
+ # Triggers
5
+ # Stable release : git tag v1.2.3 → PyPI + GitHub Release
6
+ # Pre-release : git tag v1.2.3b1 → TestPyPI + GitHub pre-release
7
+ # Manual : Actions tab → Run workflow (choose target)
8
+ # ──────────────────────────────────────────────────────────────
9
+ on:
10
+ push:
11
+ tags:
12
+ - "v*.*.*" # v1.0.0, v1.2.3, v0.1.0 …
13
+ - "v*.*.*.*" # v1.0.0.post1 etc.
14
+
15
+ workflow_dispatch:
16
+ inputs:
17
+ target:
18
+ description: "Publish target"
19
+ required: true
20
+ default: "testpypi"
21
+ type: choice
22
+ options: [testpypi, pypi]
23
+
24
+ jobs:
25
+ # ──────────────────────────────────────────────────────────────
26
+ # Lint + test before we touch PyPI
27
+ # ──────────────────────────────────────────────────────────────
28
+ verify:
29
+ name: Verify before publish
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+
34
+ - uses: astral-sh/setup-uv@v5
35
+ with:
36
+ version: "latest"
37
+
38
+ - name: Set up Python
39
+ uses: actions/setup-python@v5
40
+ with:
41
+ python-version: "3.11"
42
+
43
+ - name: Install project + dev deps
44
+ run: uv pip install --system -e ".[dev,openai,anthropic]"
45
+
46
+ - name: Lint
47
+ run: ruff check conduit_sdk tests
48
+
49
+ - name: Tests
50
+ run: pytest tests/ --tb=short -q
51
+
52
+ # ──────────────────────────────────────────────────────────────
53
+ # Build sdist + wheel
54
+ # ──────────────────────────────────────────────────────────────
55
+ build:
56
+ name: Build distribution
57
+ runs-on: ubuntu-latest
58
+ needs: verify
59
+ steps:
60
+ - uses: actions/checkout@v4
61
+
62
+ - uses: astral-sh/setup-uv@v5
63
+ with:
64
+ version: "latest"
65
+
66
+ - name: Set up Python
67
+ uses: actions/setup-python@v5
68
+ with:
69
+ python-version: "3.11"
70
+
71
+ - name: Install build tools
72
+ run: uv pip install --system build twine
73
+
74
+ - name: Build sdist and wheel
75
+ run: python -m build
76
+
77
+ - name: Check distribution with twine
78
+ run: twine check dist/*
79
+
80
+ - name: Upload distribution artifacts
81
+ uses: actions/upload-artifact@v4
82
+ with:
83
+ name: dist
84
+ path: dist/
85
+ retention-days: 7
86
+
87
+ # ──────────────────────────────────────────────────────────────
88
+ # Detect whether this tag is a pre-release
89
+ # PEP 440 pre-releases: a/b/rc suffix e.g. v1.0.0a1, v1.0.0b2, v1.0.0rc1
90
+ # ──────────────────────────────────────────────────────────────
91
+ classify-tag:
92
+ name: Classify tag
93
+ runs-on: ubuntu-latest
94
+ if: github.event_name == 'push'
95
+ outputs:
96
+ is_prerelease: ${{ steps.check.outputs.is_prerelease }}
97
+ steps:
98
+ - name: Check if pre-release tag
99
+ id: check
100
+ run: |
101
+ TAG="${{ github.ref_name }}"
102
+ if echo "$TAG" | grep -qE '(a|b|rc|alpha|beta|dev)[0-9]*$'; then
103
+ echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
104
+ else
105
+ echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
106
+ fi
107
+
108
+ # ──────────────────────────────────────────────────────────────
109
+ # Publish stable releases → PyPI
110
+ # ──────────────────────────────────────────────────────────────
111
+ publish-pypi:
112
+ name: Publish → PyPI
113
+ runs-on: ubuntu-latest
114
+ needs: [build, classify-tag]
115
+ if: |
116
+ (github.event_name == 'push' && needs.classify-tag.outputs.is_prerelease == 'false') ||
117
+ (github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'pypi')
118
+ environment:
119
+ name: pypi
120
+ url: https://pypi.org/p/llm-conduit
121
+ permissions:
122
+ id-token: write # OIDC trusted publishing — no API token needed
123
+
124
+ steps:
125
+ - name: Download distribution
126
+ uses: actions/download-artifact@v4
127
+ with:
128
+ name: dist
129
+ path: dist/
130
+
131
+ - name: Publish to PyPI
132
+ uses: pypa/gh-action-pypi-publish@release/v1
133
+
134
+ # ──────────────────────────────────────────────────────────────
135
+ # Publish pre-releases → TestPyPI
136
+ # ──────────────────────────────────────────────────────────────
137
+ publish-testpypi:
138
+ name: Publish → TestPyPI
139
+ runs-on: ubuntu-latest
140
+ needs: [build, classify-tag]
141
+ if: |
142
+ (github.event_name == 'push' && needs.classify-tag.outputs.is_prerelease == 'true') ||
143
+ (github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'testpypi')
144
+ environment:
145
+ name: testpypi
146
+ url: https://test.pypi.org/p/llm-conduit
147
+ permissions:
148
+ id-token: write
149
+
150
+ steps:
151
+ - name: Download distribution
152
+ uses: actions/download-artifact@v4
153
+ with:
154
+ name: dist
155
+ path: dist/
156
+
157
+ - name: Publish to TestPyPI
158
+ uses: pypa/gh-action-pypi-publish@release/v1
159
+ with:
160
+ repository-url: https://test.pypi.org/legacy/
161
+
162
+ # ──────────────────────────────────────────────────────────────
163
+ # Auto-create GitHub Release after successful publish
164
+ # ──────────────────────────────────────────────────────────────
165
+ github-release:
166
+ name: Create GitHub Release
167
+ runs-on: ubuntu-latest
168
+ needs: [publish-pypi, classify-tag]
169
+ if: |
170
+ always() &&
171
+ github.event_name == 'push' &&
172
+ (needs.publish-pypi.result == 'success' || needs.publish-testpypi.result == 'success')
173
+ permissions:
174
+ contents: write # needed to create releases
175
+
176
+ steps:
177
+ - uses: actions/checkout@v4
178
+
179
+ - name: Download distribution
180
+ uses: actions/download-artifact@v4
181
+ with:
182
+ name: dist
183
+ path: dist/
184
+
185
+ - name: Create GitHub Release
186
+ uses: softprops/action-gh-release@v2
187
+ with:
188
+ tag_name: ${{ github.ref_name }}
189
+ name: ${{ github.ref_name }}
190
+ prerelease: ${{ needs.classify-tag.outputs.is_prerelease == 'true' }}
191
+ generate_release_notes: true # auto-fills from PR titles & commits
192
+ files: dist/*