Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions api/doc/settings/put-req/.type/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ export type ProviderID6 = string;
export type DisplayName6 = string;
export type Enabled6 = boolean;
export type APIKey6 = string;
/**
* Optional. The Scaleway Project ID (UUID) the API key is scoped to. Required when the key only has access to a specific project, otherwise model listing and inference return 403.
*/
export type ProjectID = string;
export type ProviderType7 = "openai-compatible";
export type ProviderID7 = string;
export type DisplayName7 = string;
Expand Down Expand Up @@ -160,12 +164,16 @@ export type Ollama = {
baseURL: BaseURL;
[k: string]: unknown;
}
/**
* For an API key scoped to a specific Scaleway Project, set the Project ID so requests target that project. Leave it empty to use the organization default project.
*/
export type Scaleway = {
type: ProviderType6;
id: ProviderID6;
name: DisplayName6;
enabled: Enabled6;
apiKey: APIKey6;
projectId?: ProjectID;
[k: string]: unknown;
}
/**
Expand Down
1,250 changes: 631 additions & 619 deletions api/doc/settings/put-req/.type/validate.js

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion api/src/models/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ import { createEvaluatorMockLanguageModel } from './evaluator-mock-model.ts'

export { createMockLanguageModel, createEvaluatorMockLanguageModel }

/**
* Scaleway's Generative APIs are reached at https://api.scaleway.ai/v1, but an
* API key scoped to a single Project must target the project-scoped URL
* https://api.scaleway.ai/{projectId}/v1 — otherwise both model listing and
* inference return 403 "insufficient permissions to access the resource".
*/
export function scalewayBaseURL (projectId?: string): string {
const trimmed = projectId?.trim()
return trimmed ? `https://api.scaleway.ai/${trimmed}/v1` : 'https://api.scaleway.ai/v1'
}

export function createModel (provider: Provider, modelId: string): LanguageModel {
switch (provider.type) {
case 'openai':
Expand All @@ -31,7 +42,9 @@ export function createModel (provider: Provider, modelId: string): LanguageModel
case 'ollama':
return createOllama({ baseURL: provider.baseURL })(modelId)
case 'scaleway':
return createOpenAI({ apiKey: provider.apiKey, baseURL: 'https://api.scaleway.ai/v1' })(modelId)
// Scaleway does not implement the OpenAI /v1/responses endpoint that the
// default callable targets; use the /v1/chat/completions model via .chat().
return createOpenAI({ apiKey: provider.apiKey, baseURL: scalewayBaseURL(provider.projectId) }).chat(modelId)
case 'openai-compatible': {
const openai = createOpenAI({
apiKey: provider.apiKey,
Expand Down
3 changes: 2 additions & 1 deletion api/src/models/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Ollama } from 'ollama'
import memoize from 'memoizee'
import axios from 'axios'
import type { Model, Provider, Settings } from '#types'
import { scalewayBaseURL } from './operations.ts'

const router = Router()
export default router
Expand Down Expand Up @@ -157,7 +158,7 @@ async function fetchModelsForProvider (
case 'openrouter':
return fetchOpenRouterModels(provider.apiKey)
case 'scaleway':
return fetchOpenAICompatibleModels('https://api.scaleway.ai/v1', provider.apiKey)
return fetchOpenAICompatibleModels(scalewayBaseURL(provider.projectId), provider.apiKey)
default:
return []
}
Expand Down
18 changes: 18 additions & 0 deletions api/types/settings/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,11 @@ export default {
}, {
required: ['type', 'name', 'id', 'enabled', 'apiKey'],
title: 'Scaleway',
description: 'For an API key scoped to a specific Scaleway Project, set the Project ID so requests target that project. Leave it empty to use the organization default project.',
'x-i18n-description': {
en: 'For an API key scoped to a specific Scaleway Project, set the Project ID so requests target that project. Leave it empty to use the organization default project.',
fr: "Pour une clé API liée à un Projet Scaleway spécifique, renseignez l'ID du projet afin que les requêtes ciblent ce projet. Laissez vide pour utiliser le projet par défaut de l'organisation."
},
properties: {
type: {
type: 'string',
Expand Down Expand Up @@ -483,6 +488,19 @@ export default {
en: 'API Key',
fr: 'Clé API'
}
},
projectId: {
type: 'string',
title: 'Project ID',
'x-i18n-title': {
en: 'Project ID',
fr: 'ID du projet'
},
description: 'Optional. The Scaleway Project ID (UUID) the API key is scoped to. Required when the key only has access to a specific project, otherwise model listing and inference return 403.',
'x-i18n-description': {
en: 'Optional. The Scaleway Project ID (UUID) the API key is scoped to. Required when the key only has access to a specific project, otherwise model listing and inference return 403.',
fr: "Optionnel. L'ID du Projet Scaleway (UUID) auquel la clé API est liée. Requis lorsque la clé n'a accès qu'à un projet spécifique, sinon le listing des modèles et l'inférence renvoient une erreur 403."
}
}
}
}, {
Expand Down
32 changes: 32 additions & 0 deletions tests/features/models/models.unit.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* stateless unit tests for pure model helpers (api/src/models/operations.ts)
*/

import { test } from 'playwright/test'
import assert from 'node:assert/strict'
import { scalewayBaseURL } from '../../../api/src/models/operations.ts'

test.describe('Scaleway base URL', () => {
test('uses the bare /v1 endpoint when no project is set', () => {
assert.equal(scalewayBaseURL(), 'https://api.scaleway.ai/v1')
assert.equal(scalewayBaseURL(''), 'https://api.scaleway.ai/v1')
})

test('uses the project-scoped endpoint when a project id is set', () => {
assert.equal(
scalewayBaseURL('9a812a7b-b670-453c-9af4-962f320a0a66'),
'https://api.scaleway.ai/9a812a7b-b670-453c-9af4-962f320a0a66/v1'
)
})

test('trims surrounding whitespace from the project id', () => {
assert.equal(
scalewayBaseURL(' abc-123 '),
'https://api.scaleway.ai/abc-123/v1'
)
})

test('treats a whitespace-only project id as unset', () => {
assert.equal(scalewayBaseURL(' '), 'https://api.scaleway.ai/v1')
})
})
3,912 changes: 2,002 additions & 1,910 deletions ui/src/components/vjsf/vjsf-put-req-en.vue

Large diffs are not rendered by default.

3,912 changes: 2,002 additions & 1,910 deletions ui/src/components/vjsf/vjsf-put-req-fr.vue

Large diffs are not rendered by default.

Loading