Skip to content

Commit c478766

Browse files
jjchen01hiento09locnguyen1986
authored
Release v0.0.10 (#121)
* Feat: support wildcard cors (#111) * feat: support wildcard cors * update docs * Feat: /jan/v1/conversation (#110) * expose endpoint * refine /conversations * revamp /jan/v1/conversation * remove /jan * resolve conflict * feat: loadtest * add README (#116) * Provide auth service (#115) * provide auth service * rename it to getUserPublicIDFromJWT * fix format typo (#117) fix format typo * Fix/mcp cors (#118) * mcp cors * expose mcp session id * remove server session id * auth for mcp * auto reloader (#120) --------- Co-authored-by: hiento09 <136591877+hiento09@users.noreply.github.com> Co-authored-by: locnguyen1986 <locnguyen1986@gmail.com>
1 parent d9e22f6 commit c478766

73 files changed

Lines changed: 6903 additions & 7565 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/load-test.yml

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
name: Load Test
2+
3+
on:
4+
# Manual trigger
5+
workflow_dispatch:
6+
inputs:
7+
test_case:
8+
description: 'Test case to run (leave empty to run all tests)'
9+
required: false
10+
default: ''
11+
type: choice
12+
options:
13+
- ''
14+
- chat-completion
15+
base_url:
16+
description: 'Base URL for testing'
17+
required: true
18+
default: 'https://api.jan.ai'
19+
type: string
20+
model:
21+
description: 'Model to test'
22+
required: true
23+
default: 'jan-v1-4b'
24+
type: string
25+
duration_minutes:
26+
description: 'Test duration in minutes'
27+
required: true
28+
default: '5'
29+
type: string
30+
nonstream_rps:
31+
description: 'Non-streaming requests per second'
32+
required: true
33+
default: '2'
34+
type: string
35+
stream_rps:
36+
description: 'Streaming requests per second'
37+
required: true
38+
default: '1'
39+
type: string
40+
41+
42+
env:
43+
# Test configuration - use inputs for workflow_dispatch, defaults for push
44+
BASE: ${{ github.event.inputs.base_url || 'https://api.jan.ai' }}
45+
MODEL: ${{ github.event.inputs.model || 'jan-v1-4b' }}
46+
DURATION_MIN: ${{ github.event.inputs.duration_minutes || '2' }}
47+
NONSTREAM_RPS: ${{ github.event.inputs.nonstream_rps || '2' }}
48+
STREAM_RPS: ${{ github.event.inputs.stream_rps || '1' }}
49+
50+
# Authentication (set these as repository secrets)
51+
API_KEY: ${{ secrets.LOADTEST_API_KEY }}
52+
LOADTEST_TOKEN: ${{ secrets.LOADTEST_TOKEN }}
53+
54+
# Prometheus remote write configuration (k6 standard env vars)
55+
K6_PROMETHEUS_RW_SERVER_URL: ${{ secrets.K6_PROMETHEUS_RW_SERVER_URL }}
56+
K6_PROMETHEUS_RW_USERNAME: ${{ secrets.K6_PROMETHEUS_RW_USERNAME }}
57+
K6_PROMETHEUS_RW_PASSWORD: ${{ secrets.K6_PROMETHEUS_RW_PASSWORD }}
58+
K6_PROMETHEUS_RW_TREND_STATS: ${{ vars.K6_PROMETHEUS_RW_TREND_STATS || 'p(95),p(99),min,max' }}
59+
K6_PROMETHEUS_RW_PUSH_INTERVAL: ${{ vars.K6_PROMETHEUS_RW_PUSH_INTERVAL || '5s' }}
60+
61+
jobs:
62+
load-test:
63+
runs-on: ubuntu-latest
64+
65+
steps:
66+
- name: Checkout code
67+
uses: actions/checkout@v4
68+
69+
- name: Setup k6
70+
uses: grafana/setup-k6-action@v1
71+
72+
- name: Clear k6 defaults
73+
run: |
74+
# Clear any potential k6 config files that might have localhost defaults
75+
rm -f ~/.k6rc || true
76+
rm -f .k6rc || true
77+
# Unset any k6 environment variables that might interfere
78+
unset K6_OUT || true
79+
unset K6_PROMETHEUS_URL || true
80+
81+
- name: Install jq for metrics parsing
82+
run: sudo apt-get update && sudo apt-get install -y jq
83+
84+
- name: Validate inputs
85+
run: |
86+
echo "🚀 Load Test Execution"
87+
echo "Trigger: ${{ github.event_name }}"
88+
echo ""
89+
echo "Test Configuration:"
90+
if [[ -n "${{ github.event.inputs.test_case }}" ]]; then
91+
echo " Test Case: ${{ github.event.inputs.test_case }} (specific test)"
92+
else
93+
echo " Test Case: ALL TESTS (manual trigger)"
94+
fi
95+
echo " Base URL: ${{ github.event.inputs.base_url }}"
96+
echo " Model: ${{ github.event.inputs.model }}"
97+
echo " Duration: ${{ github.event.inputs.duration_minutes }} minutes"
98+
echo " Non-stream RPS: ${{ github.event.inputs.nonstream_rps }}"
99+
echo " Stream RPS: ${{ github.event.inputs.stream_rps }}"
100+
101+
# Validate required secrets
102+
if [[ -z "$API_KEY" && -z "$LOADTEST_TOKEN" ]]; then
103+
echo "⚠️ Warning: Neither API_KEY nor LOADTEST_TOKEN is configured"
104+
fi
105+
106+
echo ""
107+
echo "Prometheus Configuration:"
108+
if [[ -n "$K6_PROMETHEUS_RW_SERVER_URL" ]]; then
109+
echo "✅ k6 Prometheus remote write endpoint configured: [CONFIGURED]"
110+
if [[ -n "$K6_PROMETHEUS_RW_USERNAME" ]]; then
111+
echo "✅ k6 Prometheus username configured: [CONFIGURED]"
112+
else
113+
echo "⚠️ k6 Prometheus username not configured"
114+
fi
115+
if [[ -n "$K6_PROMETHEUS_RW_PASSWORD" ]]; then
116+
echo "✅ k6 Prometheus password configured: [HIDDEN]"
117+
else
118+
echo "⚠️ k6 Prometheus password not configured"
119+
fi
120+
echo "📊 Trend stats: $K6_PROMETHEUS_RW_TREND_STATS"
121+
echo "⏱️ Push interval: $K6_PROMETHEUS_RW_PUSH_INTERVAL"
122+
else
123+
echo "⚠️ Warning: K6_PROMETHEUS_RW_SERVER_URL is not configured"
124+
fi
125+
126+
- name: Run load test
127+
id: loadtest
128+
run: |
129+
cd tests
130+
if [[ "${{ github.event_name }}" == "workflow_dispatch" && -n "${{ github.event.inputs.test_case }}" ]]; then
131+
echo "Running specific test case: ${{ github.event.inputs.test_case }}"
132+
./run-loadtest.sh ${{ github.event.inputs.test_case }}
133+
else
134+
echo "Running all test cases"
135+
./run-loadtest.sh
136+
fi
137+
138+
- name: Parse test results
139+
id: parse_results
140+
if: always()
141+
run: |
142+
cd tests/results
143+
144+
# Find the latest results file
145+
LATEST_FILE=$(ls -t *_*.json 2>/dev/null | head -1 || echo "")
146+
147+
if [[ -n "$LATEST_FILE" && -f "$LATEST_FILE" ]]; then
148+
echo "results_file=$LATEST_FILE" >> $GITHUB_OUTPUT
149+
150+
# Extract key metrics using jq
151+
if command -v jq &> /dev/null; then
152+
echo "=== Load Test Results ===" >> results_summary.txt
153+
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
154+
echo "Test Case: ${{ github.event.inputs.test_case || 'All Tests' }}" >> results_summary.txt
155+
echo "Duration: ${{ github.event.inputs.duration_minutes || '2' }} minutes" >> results_summary.txt
156+
else
157+
echo "Test Case: All Tests (auto-triggered)" >> results_summary.txt
158+
echo "Duration: $DURATION_MIN minutes" >> results_summary.txt
159+
fi
160+
echo "Trigger: ${{ github.event_name }}" >> results_summary.txt
161+
echo "Date: $(date)" >> results_summary.txt
162+
echo "" >> results_summary.txt
163+
164+
# Parse metrics
165+
jq -r '.metrics | to_entries[] | select(.key | contains("llm_")) | "\(.key): avg=\(.value.avg // "N/A"), min=\(.value.min // "N/A"), max=\(.value.max // "N/A"), p95=\(.value.p95 // "N/A")"' "$LATEST_FILE" >> results_summary.txt 2>/dev/null || echo "Failed to parse detailed metrics" >> results_summary.txt
166+
167+
# Check for errors
168+
ERROR_COUNT=$(jq -r '.metrics.llm_errors.count // 0' "$LATEST_FILE" 2>/dev/null || echo "0")
169+
echo "" >> results_summary.txt
170+
echo "Error Count: $ERROR_COUNT" >> results_summary.txt
171+
172+
# Set output for next steps
173+
echo "error_count=$ERROR_COUNT" >> $GITHUB_OUTPUT
174+
175+
# Display summary
176+
echo "=== Test Results Summary ==="
177+
cat results_summary.txt
178+
else
179+
echo "jq not available, skipping detailed metrics parsing"
180+
fi
181+
else
182+
echo "No results file found"
183+
echo "error_count=999" >> $GITHUB_OUTPUT
184+
fi
185+
186+
- name: Upload test results
187+
uses: actions/upload-artifact@v4
188+
if: always()
189+
with:
190+
name: loadtest-results-${{ github.event_name == 'workflow_dispatch' && github.event.inputs.test_case || 'all-tests' }}-${{ github.run_number }}
191+
path: |
192+
tests/results/
193+
194+
- name: Fail job if errors detected
195+
if: steps.parse_results.outputs.error_count != '0'
196+
run: |
197+
echo "❌ Load test detected ${{ steps.parse_results.outputs.error_count }} errors"
198+
exit 1
199+
200+
- name: Success notification
201+
if: success()
202+
run: |
203+
echo "✅ Load test completed successfully!"
204+
echo "Trigger: ${{ github.event_name }}"
205+
if [[ "${{ github.event_name }}" == "workflow_dispatch" && -n "${{ github.event.inputs.test_case }}" ]]; then
206+
echo "Test case: ${{ github.event.inputs.test_case }}"
207+
else
208+
echo "Test case: All tests"
209+
fi
210+
echo "Error count: ${{ steps.parse_results.outputs.error_count }}"

apps/jan-api-gateway/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ A comprehensive API gateway for Jan Server that provides OpenAI-compatible endpo
163163
| `OAUTH2_GOOGLE_CLIENT_ID` | Google OAuth2 client ID | `your-google-client-id` |
164164
| `OAUTH2_GOOGLE_CLIENT_SECRET` | Google OAuth2 client secret | `your-google-client-secret` |
165165
| `OAUTH2_GOOGLE_REDIRECT_URL` | Google OAuth2 redirect URL | `http://localhost:8080/auth/google/callback` |
166-
| `ALLOWED_CORS_HOSTS` | allowed cors hosts | `http://localhost:8080` |
166+
| `ALLOWED_CORS_HOSTS` | Value of allowed CORS hosts, separated by commas, supporting prefix wildcards with '*'. | `http://localhost:8080,*jan.ai` |
167167

168168
## 📚 API Usage Examples
169169

apps/jan-api-gateway/application/app/domain/apikey/apikey.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ type ApiKey struct {
2525
Description string
2626
Enabled bool
2727
ApikeyType string // "admin","project","service","organization","ephemeral"
28-
OwnerID *uint
28+
OwnerPublicID string
2929
ProjectID *uint
3030
OrganizationID *uint
3131
Permissions string //json
@@ -54,7 +54,7 @@ type ApiKeyFilter struct {
5454
KeyHash *string
5555
PublicID *string
5656
ApikeyType *string
57-
OwnerID *uint
57+
OwnerPublicID *string
5858
ProjectID *uint
5959
UserID *uint
6060
OrganizationID *uint

apps/jan-api-gateway/application/app/domain/apikey/apikey_service.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,16 @@ func NewService(repo ApiKeyRepository) *ApiKeyService {
2222
}
2323
}
2424

25+
const ApikeyPrefix = "sk"
26+
2527
func (s *ApiKeyService) GenerateKeyAndHash(ctx context.Context, ownerType ApikeyType) (string, string, error) {
26-
baseKey, err := idgen.GenerateSecureID("sk", 24)
28+
baseKey, err := idgen.GenerateSecureID(ApikeyPrefix, 24)
2729
if err != nil {
2830
return "", "", err
2931
}
3032

31-
// Business rule: Format as sk_<random>-<ownerType> for identification
32-
apikey := fmt.Sprintf("%s-%s", baseKey, ownerType)
33+
// Business rule: Format as sk_<ownerType>-<random> for identification
34+
apikey := fmt.Sprintf("%s-%s", ownerType, baseKey)
3335
hash := s.HashKey(ctx, apikey)
3436
return apikey, hash, nil
3537
}

0 commit comments

Comments
 (0)