{ "title": "SKO unified integration: Google OAuth, SMTP and user GenAI configuration", "version": "2.10.0", "origin": "https://oauth.skoonline.org", "agentInstruction": "Integrate Google OAuth, SMTP and user-owned GenAI through https://oauth.skoonline.org. Follow https://oauth.skoonline.org/generalInstruction.json. Register one client with OAuth, SMTP and GenAI permissions and use its single server-side secret for all three services. Use the hosted service UI, preserve existing app identities and roles, keep credentials server-side, and verify persistence after sign-out and restart. Implement configured optional per-app GCS storage and primary/debug/webhooks xAPI LRS settings using the optionalConnections instructions; leave unused profiles inactive. For personal text inference use backend-cached execution sessions and direct provider calls; follow directExecution below. Do not claim live deployment from a source commit.", "links": { "html": "https://oauth.skoonline.org/generalInstruction", "json": "https://oauth.skoonline.org/generalInstruction.json", "text": "https://oauth.skoonline.org/generalInstruction.txt", "module": "https://oauth.skoonline.org/genaiInstruction/ui/v1/services.js" }, "directExecution": { "standard": "Cache an explicitly authorized personal text execution session on the consuming app backend and call the provider directly; no per-generation gateway inference request.", "endpoint": "https://oauth.skoonline.org/genai/v1/execution-session", "sdkHelper": "https://oauth.skoonline.org/genaiInstruction/direct-execution.js", "cache": "Verified user/session scope, up to 15 minutes; coalesce loads; invalidate after local settings changes, reconnect, disconnect and logout.", "limits": "Requires fresh explicit direct-execution consent. No shared hosted-key export or central credit enforcement. Provider keys remain server-only; rotating them is required to revoke keys already handed out. Existing app adapters must be migrated individually." }, "optionalConnections": { "precedence": "Register optional GCS and primary/debug/webhooks LRS settings at oauth.skoonline.org. In each app set USE_OAUTH_GCS=true to use registered storage and USE_OAUTH_LRS=true to use registered LRS profiles. These switches are independent; false or unset uses that app's original configuration. The backend calls POST /genai/services/config with Basic PROXY_CLIENT_ID:PROXY_CLIENT_SECRET and JSON {gcs:true,lrs:true} matching enabled switches. Use loadOptionalConnections from genai/sdk/optional-connections.js at server startup; it retrieves only requested settings. A complete gateway profile takes precedence, otherwise the complete original profile is the fallback. Never mix profiles. Network errors, HTTP 429 and 5xx use original configuration; 4xx authentication/configuration errors are surfaced. This selects destinations at initialization; it does not migrate GCS data or perform per-operation storage failover. Keep credentials and responses server-side.", "setup": "In Connect a new app or App settings, expand Optional storage and xAPI LRS connections. All four connections and every field are optional. Each LRS has URL, User, and Secret / Password. Leave unused settings blank.", "delivery": "These settings are per-app deployment configuration. Set USE_OAUTH_GCS and USE_OAUTH_LRS independently on the app server, then load the registered configuration with the SDK helper. The app backend retrieves the requested settings from POST /genai/services/config using its existing client credentials; OAuth tokens and GenAI grants do not include these settings.", "variables": { "USE_OAUTH_GCS": "true selects registered bucket; false/unset uses original", "USE_OAUTH_LRS": "true selects registered LRS profiles; false/unset uses originals", "credentials": [ "OAUTH_BASE_URL", "PROXY_CLIENT_ID", "PROXY_CLIENT_SECRET" ], "originalBucket": "GCS_BUCKET", "originalLRSPrefixes": [ "XAPI_LRS", "DEBUG_XAPI_LRS", "WEBHOOK_XAPI_LRS" ] }, "storage": "Use the app runtime Google Cloud service account/Application Default Credentials with @google-cloud/storage. GCS_BUCKET is a bucket name, not gs:// or a URL. Grant appropriate private bucket permissions separately. An unset bucket leaves existing storage behavior unchanged; never silently replace durable storage with memory.", "xapi": "Read each LRS independently on the app backend. An empty or partial gateway profile uses the complete original profile as fallback. If neither is complete, keep that connection inactive. Construct Basic authentication from base64(User + \":\" + Secret/Password). Append statements to the configured xAPI base URL after normalizing its trailing slash. Send Content-Type: application/json and X-Experience-API-Version: 1.0.3 for xAPI 1.0.3 statements. Use valid actor, verb and object; obtain learner identity from a verified app session.", "routing": "Primary receives intended learning/assessment statements; debug receives explicitly enabled diagnostics with minimized data; webhooks receives statements for verified webhook events. Do not fan out automatically or fall back between profiles. The webhook LRS is an outbound destination, not an incoming webhook registration; authenticate incoming webhooks in the receiving app.", "reliability": "Generate and persist a statement UUID before delivery. For retry-safe xAPI 1.0.3 delivery, PUT /statements?statementId= with the same statement. Use bounded timeouts and an app-owned durable outbox for retries; classify failures and avoid blocking the learning activity. Do not claim delivery until HTTP success.", "secrets": "Never expose LRS credentials in browser bundles, public JSON, URLs, logs or commits. The administrator-only app detail route decrypts saved values for editing; lists and usage omit them. Copy into server secret configuration. Existing config updates omitting optionalConnections preserve values; explicit blanks clear fields.", "verification": [ "All profiles blank: existing OAuth, SMTP, GenAI and learning flow still work.", "Bucket configured: explicitly verify private read/write with the app service account.", "Each LRS: explicit synthetic test, validate its target and authentication, confirm receipt independently.", "Check partial settings, invalid credentials, timeout and repeated delivery of the same statement ID.", "Check webhook signature/authentication and deduplicate the incoming event before creating a statement." ] }, "connectionTesting": { "widget": "Supply genai.testConnection(id) as a backend adapter. Your AI offers Test generation for text and Check provider / model for other saved capabilities. No test runs automatically.", "endpoint": "POST /genai/v1/connections/:id/test with user Bearer token and app X-Client-Authorization. Enforce app-session authentication and exact Origin at the calling app adapter. The proxy checks current grant scope before testing.", "results": "generation_verified means a small text generation returned nonempty text; normal accounting applies. model_listed means discovery authenticated and included the model, not that generation works. unverified or unsupported never means success. Results are point-in-time and no model fallback occurs." }, "personalAIConfiguration": { "version": "1.0.0", "portability": "The portal wizard exports credential-free configuration JSON and imports it with explicit connection mapping and review. POST /genai/api/configuration/import-preview accepts {document,mapping?}; it returns ready, requirements and a current-revision configuration without saving. Apply through revision-checked PUT /genai/api/configuration. Names do not establish ownership; create missing connections separately. Keys, grants and tokens are never imported.", "schema": "https://oauth.skoonline.org/genai/schemas/personal-ai-use-configuration.v1.json", "portal": "GET/PUT /genai/api/configuration. Revision-safe, credential-free connection inventory plus uses[capability]: {defaultConnection,backupConnections,fallbackMode}. Omitted uses are preserved. Inventory is server-owned; create credentials through the connection wizard.", "apps": "GET /genai/v1/connections includes configuration scoped to the current grant and effective routing. Existing APIs remain compatible. Use /genai/v1/routing for app overrides, not portal-wide configuration writes.", "migration": "Existing profiles/defaults/backups project to schema v1 without deleting credentials. Schema metadata drives wizard categories, source groups and stages. Unknown schema versions are rejected; consent and execution validation remain separate." }, "intendedUses": { "request": "Send intended_uses: [\"text\",\"image\",\"speech\"] from the app backend on POST /genai/services/sso/start, alongside its existing proof, expected_email, state, callback and PKCE fields. Full-page /genai/connect accepts intended_uses=text,image,speech. SDK: link(callback, {intendedUses:[\"text\",\"speech\"]}).", "identifiers": [ "text", "image", "speech", "transcription", "realtime", "video", "embeddings" ], "behavior": "The wizard focuses inventory, intended-use assignments, connection capabilities and consent suggestions on the requested uses. speech means TTS. Omitted context preserves all uses for older clients; unknown or empty lists return 400. Other saved user configurations remain unchanged.", "security": "This is setup context, not permission or execution support. User consent, connection scope, capability checks and verified identity remain mandatory. Configure requested uses on the app server and align the shared widget capabilities with implemented execution support." }, "capabilityFiltering": { "behavior": "Select an AI use before provider/model. Provider templates are filtered by configuration hints; other providers remain available behind Show other providers (unverified). Discovery receives capability and returns matching models, unverifiedModels and excludedModels. Metadata and conservative model-family rules guide filtering, not account permissions or live execution verification.", "unknowns": "Users can explicitly show unverified models or enter a deployment ID. Known incompatible OpenAI model families are rejected on save. Input vision support is not image output support. Voice output metadata alone does not establish transcription or realtime endpoint compatibility.", "futureWorkflow": "An LLM may recommend next steps using discovered choices, but its recommendation is not provider capability evidence. Validate actual IDs, schemas, credentials and authorization independently before saving or executing." }, "setupWorkflow": { "assistant": "Set LLM_PROVIDER, LLM_MODEL, LLM_ENDPOINT and LLM_API_KEY on the proxy. For azure, an empty LLM_API_KEY falls back to AZURE_OPENAI_API_KEY; model/endpoint also accept Azure variables. AZURE_OPENAI_API_VERSION is retained. A blank GENAI_MAX_INFLIGHT defaults to 12.", "allowance": "LLM_ALLOWANCE_TOKENS defaults to 100000 tokens per authenticated user per UTC month. Assistant usage is isolated from personal connection balances; exhaustion blocks further assistance. Zero internal tariffs do not mean Azure is free. No automatic backup or retry is implemented.", "flow": "Open Add connection → Let AI guide my setup. Describe the goal without secrets. The server assistant recommends a validated capability/provider and next steps. User explicitly applies the suggestion, then enters credentials, discovers models and reviews before saving. Recommendations are not live verification.", "privacy": "Goal descriptions are sent to the configured assistant; API keys and draft connection values are not. The assistant is not exposed as a connection that calling apps can authorize." }, "setupAssistance": { "behavior": "The hosted wizard explains intended uses and every configuration field. Optional values default to provider behavior. Field-level Ask AI is opt-in, using a user-selected saved or hosted text connection with normal usage accounting. Without a text connection, static guidance remains available.", "privacy": "Only allowlisted field name, capability and provider metadata go into assistance prompts. Draft keys, endpoints and field values are excluded. AI advice is displayed as text and never automatically applied; model lists and provider documentation remain authoritative." }, "aiRouting": { "capabilities": [ "text", "image", "speech", "transcription", "realtime", "video", "embeddings" ], "portal": "https://oauth.skoonline.org/genai/", "setup": "In the centrally hosted setup portal, use the Defaults & backups wizard: AI use → Default & backups → Review → Save. Add missing connections through its provider/model wizard, then return to assign roles. Save one default plus up to three ordered backups per capability. Up to 100 connections can be stored. Voice/language/format and other capability settings are edited on each connection.", "read": "GET /genai/v1/connections returns routing[capability]: {primary,backups,fallbackMode,source,needsDefault}. Connections are filtered by the current app grant, current existence and matching capability. An unavailable default remains empty: never silently promote a backup.", "override": "PUT /genai/v1/routing with {capability,primary,backups} saves an app-specific override for this user. Send {capability,inherit:true} to restore their shared preferences. Use user Bearer token and X-Client-Authorization on the backend. SDK: setRouting(accessToken, options).", "execution": "Routing is a configuration contract. Existing selection APIs retain their behavior. Apps adopting routing must explicitly use routing.primary as the request connection. Backups require an explicit user choice; do not retry ambiguous failures, switch vendors automatically or assume modality execution support. Embedding models/dimensions must remain compatible with the index.", "authorization": "Grant approval is required independently for every primary and backup. Never expose keys. Account preferences do not authorize every app under the domain. Configuration of image/audio/video does not imply that inference for that capability is implemented by this gateway." }, "sharedSettingsShell": { "connectionDisplay": "Your AI lists each returned authorized connection with model, configured capability, intended use and app selection status. Pass optional intendedUse for a connection-specific description; otherwise the widget supplies capability-based guidance. Pass routing to show default/backup roles. Unsupported capabilities are labeled, not offered as selectors.", "module": "https://oauth.skoonline.org/genaiInstruction/ui/v1/services.js", "export": "mountAccountLauncher", "contract": "mountAccountLauncher(host, {account, smtp, genai, appModels, t}) returns {open, refresh, destroy}. The proxy owns the launcher, native dialog, header, tabs, scrolling, service widgets and app-model selector. Mount once and destroy on unmount. Do not reproduce this UI in the calling app.", "appModels": "Optional {load, select, openLocal}. load returns {models:[{id,displayName,available,locked}],current,disabledReason,localWizard,message}. select(id) persists through the app backend. openLocal() optionally returns a promise resolving when the app-specific local-runtime wizard closes. This legacy wizard is the only app-rendered extension.", "identity": "Keep the proxy receipt in the authenticated app session. During renewal bind expected_email to that session, verify the returned identity before replacing it, and pass expected_email to the SSO start endpoint. Never use an email address as proof of identity. A standalone proxy administrator session must not replace the app user.", "rollout": "Deploy the proxy before apps. Apps must display a retryable error if mountAccountLauncher is missing, rather than navigate to a standalone portal session." }, "embeddedSSO": { "purpose": "Open personal GenAI setup in a near-full-screen proxy-hosted modal without repeating Google login.", "login": "The OAuth token exchange additionally returns sso_receipt and sso_expires_in. Encrypt the receipt in the calling app session; never put it in browser storage, URLs or user profile JSON. It proves a prior proxy-verified login; an app secret or email alone is not sufficient.", "emailLogin": "For email/password sessions authenticated by this gateway /user/login, send its signed token as email_token instead of receipt, only from the app backend with client Basic authentication and expected_email from the server session. Never forward passwords or accept identity tokens from request bodies. Expired email tokens require email/password sign-in again, not Google renewal. Email identities are separate from Google identities and never inherit portal admin privileges.", "start": { "method": "POST", "url": "https://oauth.skoonline.org/genai/services/sso/start", "authorization": "Basic base64(clientId:clientSecret)", "body": [ "receipt OR email_token", "expected_email", "redirect_uri", "state", "code_challenge", "code_challenge_method" ], "response": [ "action", "ticket" ] }, "browser": "From an authenticated same-origin POST in the calling app, obtain the handoff and call openGenAISetup({handoff,t}) exported by the hosted services.js. The widget POSTs the one-use 60-second ticket into its modal iframe. Only same-site HTTPS sibling apps with an exact registered GenAI callback are supported.", "callback": "Verify the existing user, browser session, state and PKCE, exchange the code server-side and encrypt the grant as usual. For iframe requests, return same-origin HTML containing , or a safe error code in data-result. Set frame-ancestors self and no-store. The shared modal reads this marker only after the frame returns to the calling app origin.", "limits": "Deploy the proxy first, then the calling app. Pre-upgrade or expired sessions require one fresh app login. Embedded sessions cannot administer the proxy. Client disablement or secret rotation invalidates handoffs. User approval of AI access remains explicit. SMTP uses app authentication and needs no individual sign-in." }, "deployment": { "gateway": { "BASE_URL": "https://oauth.skoonline.org", "STORAGE_TYPE": "gcs", "GCS_BUCKET_NAME": "YOUR_EXISTING_PRIVATE_BUCKET", "GENAI_ENABLED": "true", "GENAI_PUBLIC_ORIGIN": "Optional; inherits BASE_URL", "GENAI_GCS_BUCKET": "Leave unset to inherit GCS_BUCKET_NAME", "API_KEY_ENCRYPTION_SECRET": "Stable secret, at least 32 bytes", "GOOGLE_CLIENT_ID": "Existing Google OAuth client", "GOOGLE_CLIENT_SECRET": "Existing Google OAuth secret", "JWT_SECRET": "Existing stable secret", "SESSION_SECRET": "Existing stable secret", "GMAIL_USER": "Configured sender address for app SMTP", "GMAIL_APP_PASSWORD": "Server-only Google app password for that sender" }, "googleCallbacks": [ "https://oauth.skoonline.org/auth/google/callback", "https://oauth.skoonline.org/genai/auth/callback" ], "storage": "One private bucket, separate record namespaces. GenAI uses genai/v1/. Keep encryption secrets and the existing GenAI identity namespace stable. Bucket sharing does not merge authentication scopes.", "app": { "APP_URL": "https://YOUR_APP_HOST", "OAUTH_BASE_URL": "https://oauth.skoonline.org", "PROXY_CLIENT_ID": "careTraining", "PROXY_CLIENT_SECRET": "One-time secret from proxy Administration", "GENAI_TOKEN_ENCRYPTION_KEY": "Stable local encryption key: 32 random bytes encoded as 64 hex characters; not sent to proxy", "OAUTH_ENABLED": "true", "OAUTH_AUTO_REGISTER": "true only if new Google users may register", "SMTP_ENABLED": "true", "GENAI_ORIGIN": "Optional; inherits OAUTH_BASE_URL" }, "consent": "Register exact app callback URLs. Add Google callback URLs in the Google OAuth client. Give the runtime service account access to the shared bucket. Configure sender/provider mail permissions before sending email." }, "clients": { "administration": "https://oauth.skoonline.org/genai/#administration", "setup": [ "Sign in with verified Google account xiangenhu@gmail.com. Only this account may administer the proxy.", "Register careTraining, enable oauth/smtp/genai and save the secret shown once. For an existing client, load it, save permissions and rotate its secret if needed.", "Register https://YOUR_APP_HOST/api/auth/oauth/callback under OAuth and https://YOUR_APP_HOST/api/ai/callback under GenAI. Register each exact production or development URL; no wildcards.", "Install the current server SDK before upgrading an existing client. Set PROXY_CLIENT_ID and PROXY_CLIENT_SECRET on the caller backend. Never pass the secret to shared browser JS." ], "authentication": "One client ID and secret for all three services. Send Authorization: Basic base64(clientId:secret) on OAuth start/token and SMTP send. GenAI token exchange uses the same Basic credentials. Every GenAI /v1 request additionally requires X-Client-Authorization: Basic base64(clientId:secret), alongside Authorization: Bearer . The current server SDK supplies both.", "controls": "Secrets are stored hashed and only shown at creation/rotation. Rotation invalidates the old secret and pending OAuth login tickets/codes. Disable the client or remove a service to block subsequent requests. Already-running requests may finish.", "migration": "Legacy OAuth/SMTP routes remain for existing callers; new integrations use the client-authenticated endpoints below. Existing GenAI registrations initially retain GenAI-only access. Saving their permissions enables the common-secret requirement. In careTraining PROXY_CLIENT_* takes precedence over legacy GENAI_CLIENT_*; SMTP_GATEWAY_TOKEN is unnecessary in common-client mode.", "dashboard": "Administration lists registered sites. Register new sites, manage callbacks and service permissions, rotate secrets, enable or disable sites, and open Usage for each site. Monthly UTC usage shows completed Google sign-ins, email outcomes, GenAI operations and reported tokens. Statistics start at deployment, may have index gaps and are not invoices; load every page for complete observed totals. No historical backfill.", "authorization": "The client secret identifies an application; it does not identify a person or grant access to all users. Google login still verifies the user, and GenAI access still needs that user’s explicit model consent." }, "oauth": { "start": { "method": "POST", "url": "https://oauth.skoonline.org/genai/services/oauth/start", "authentication": "Basic client credentials", "body": { "redirect_uri": "https://YOUR_APP_HOST/api/auth/oauth/callback", "state": "", "code_challenge": "", "code_challenge_method": "S256" } }, "authorize": "Redirect the browser to the returned url. The proxy signs in with Google directly and returns code and state to the registered callback. No client secret appears in browser URLs.", "token": { "method": "POST", "url": "https://oauth.skoonline.org/genai/services/oauth/token", "authentication": "Same Basic client credentials", "body": { "code": "", "redirect_uri": "https://YOUR_APP_HOST/api/auth/oauth/callback", "code_verifier": "" } }, "callback": "Check state against the initiating browser session, exchange the code from your backend, validate the verified Google identity in response.user, preserve local roles, create the app session and redirect to a clean URL. Login tickets last 10 minutes; codes last 60 seconds and are single use.", "security": "Persist and encrypt PKCE verifiers server-side. Only exact registered HTTPS callbacks are accepted. Keep secrets off the browser. Codespaces URLs must be registered exactly and updated when the hostname changes.", "sharedIdentity": "Use the verified stable subject returned by the proxy; do not use display names as identifiers." }, "smtp": { "endpoint": "https://oauth.skoonline.org/genai/services/smtp/send", "method": "POST", "authentication": "Authorization: Basic base64(PROXY_CLIENT_ID:PROXY_CLIENT_SECRET); smtp permission required", "body": { "to": "recipient@example.com", "subject": "Subject", "body": "Plain-text message", "requestId": "" }, "response": "Check HTTP success and success:true. Repeating a successful requestId with the same payload returns duplicate:true without resending. Reusing it with different contents is rejected.", "limitations": "One recipient and plain text, sent from the proxy-configured GMAIL_USER using GMAIL_APP_PASSWORD. Caller cannot override sender. Admin controls the per-client hourly limit. Pending or uncertain sends return an error and must not be automatically retried with a new requestId.", "verification": "Send a test message only after explicit operator action to a confirmed recipient. Mounting a UI component does not send mail." }, "ui": { "module": "https://oauth.skoonline.org/genaiInstruction/ui/v1/services.js", "exports": [ "mountAccountLauncher", "mountAccountSettings", "mountGoogleSignIn", "mountSMTPSettings", "mountGenAISettings", "mountServiceSettings" ], "recommended": "Use mountAccountLauncher(host, {account?, smtp?, genai?, appModels?, t?}) for the proxy-owned launcher and full settings modal. account uses {load, actions?}; smtp uses {load, actions?}; genai uses {load, select, connect, disconnect, capabilities?}. Include only services the user may access. Do not recreate headings, wrappers or controls in each app.", "example": "import {mountAccountLauncher} from 'https://oauth.skoonline.org/genaiInstruction/ui/v1/services.js';\nconst panel = mountAccountLauncher(document.querySelector('#account-settings'), {account: {load: loadAccount}, smtp: {load: loadEmailStatus}, genai: {load: loadAISettings, select: saveAISelection, connect: startAIConsent, disconnect: disconnectAI, capabilities: ['text']}});\n// Call panel.destroy() when the host view unmounts.", "adapters": "Host app supplies authenticated backend callbacks. No provider keys, service keys, app secrets or bearer tokens are passed to the UI.", "csp": "Add https://oauth.skoonline.org to script-src and style-src; retain other CSP protections. Assets and portal links resolve relative to import.meta.url.", "lifecycle": "Use separate empty host elements. Widgets isolate CSS in shadow roots, accept t(text), and return refresh()/destroy(). Keep local controls as a fallback.", "google": "mountGoogleSignIn(host, {signIn, t?}); signIn navigates to the host app backend login-start route.", "smtp": "mountSMTPSettings(host, {load, actions?, t?}); load returns {configured, connected?, message?, details?:string[]}; each action is {label,run}.", "genai": "mountGenAISettings(host, {load,select,connect,disconnect,capabilities?,t?}); load returns {configured,linked,providers,selectedProviderId,defaults,selectionUnavailable?,connectionError?}. Providers contain {id,name,model,capability}; defaults maps capability to ID. select(id,capability) persists through the backend. connect starts explicit GenAI consent; disconnect removes this app access locally.", "scope": "careTraining main adopts these controls. Other SKO apps must implement the adapters; this is not an automatic wildcard-site rollout." }, "genai": { "title": "GenAI Management Proxy — LLM Provider Selection and Integration Guide", "version": "1.1.0", "lastUpdated": "2026-11-01", "baseUrl": "https://oauth.skoonline.org", "domainPolicy": { "mode": "deployment-aware", "origin": "https://oauth.skoonline.org", "deriveFromRequest": true, "instructions": "Documentation follows the serving domain; shared UI uses import.meta.url. Server-to-server credentials and OAuth callbacks use trusted deployment configuration, never arbitrary request headers.", "deploymentRequirement": "Set BASE_URL to the deployed origin. GENAI_PUBLIC_ORIGIN is an optional override. Preserve the account namespace and encryption secrets when changing domains." }, "links": { "json": "https://oauth.skoonline.org/genaiInstruction", "jsonFile": "https://oauth.skoonline.org/genaiInstruction.json", "html": "https://oauth.skoonline.org/genaiInstruction.html", "text": "https://oauth.skoonline.org/genaiInstruction.txt", "sdk": "https://oauth.skoonline.org/genaiInstruction/sdk.js", "directExecutionHelper": "https://oauth.skoonline.org/genaiInstruction/direct-execution.js", "portal": "https://oauth.skoonline.org/genai/", "oauthAndSMTP": "https://oauth.skoonline.org/instruction" }, "preserve": [ "Existing OAuth login", "Existing user identities and permissions", "Existing SMTP integration" ], "providerSelection": { "location": "User profile → AI models and providers", "choices": [ { "id": "hosted", "label": "Built-in model", "billing": "Configured allowance or credit; requires operator setup" }, { "id": "personal", "label": "My provider and API key", "billing": "Provider bills the user directly" }, { "id": "local", "label": "My local LLM", "requirement": "Authenticated public HTTPS endpoint reachable by the gateway" } ], "catalog": [ { "id": "openai", "label": "OpenAI", "baseURL": "https://api.openai.com/v1", "docsURL": "https://platform.openai.com/docs/api-reference", "category": "Direct APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "configurationCapabilities": [ "text", "image", "speech", "transcription", "realtime", "video", "embeddings" ], "endpoints": [ { "label": "Default API", "url": "https://api.openai.com/v1" } ] }, { "id": "anthropic", "label": "Anthropic / Claude", "baseURL": "https://api.anthropic.com/v1", "docsURL": "https://docs.anthropic.com/en/api/models-list", "category": "Direct APIs", "protocol": "Anthropic Messages", "access": "direct", "discovery": true, "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://api.anthropic.com/v1" } ] }, { "id": "azure", "label": "Microsoft Azure OpenAI", "docsURL": "https://learn.microsoft.com/azure/ai-services/openai/", "category": "Direct APIs", "protocol": "Azure OpenAI", "access": "direct", "discovery": false, "notes": "Use the resource endpoint and your deployment name, not a public model name.", "configurationCapabilities": [ "text", "image", "speech", "transcription", "realtime", "video", "embeddings" ], "endpoints": [] }, { "id": "google", "label": "Google Gemini", "baseURL": "https://generativelanguage.googleapis.com/v1beta/openai", "docsURL": "https://ai.google.dev/gemini-api/docs/openai", "category": "Direct APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "configurationCapabilities": [ "text", "image", "speech", "transcription", "realtime", "video", "embeddings" ], "endpoints": [ { "label": "Default API", "url": "https://generativelanguage.googleapis.com/v1beta/openai" } ] }, { "id": "deepseek", "label": "DeepSeek", "baseURL": "https://api.deepseek.com", "docsURL": "https://api-docs.deepseek.com/", "category": "Direct APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://api.deepseek.com" } ] }, { "id": "zai", "label": "Z.ai / Zhipu (智谱) / GLM", "baseURL": "https://api.z.ai/api/paas/v4", "docsURL": "https://docs.z.ai/", "category": "Regional APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": false, "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Z.ai international", "url": "https://api.z.ai/api/paas/v4" }, { "label": "Zhipu / BigModel China mainland", "url": "https://open.bigmodel.cn/api/paas/v4" } ] }, { "id": "xai", "label": "xAI", "baseURL": "https://api.x.ai/v1", "docsURL": "https://docs.x.ai/", "category": "Direct APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "grok-4", "grok-4-fast-non-reasoning" ], "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://api.x.ai/v1" } ] }, { "id": "mistral", "label": "Mistral", "baseURL": "https://api.mistral.ai/v1", "docsURL": "https://docs.mistral.ai/", "category": "Direct APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "mistral-large-latest", "mistral-small-latest" ], "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://api.mistral.ai/v1" } ] }, { "id": "cohere", "label": "Cohere", "baseURL": "https://api.cohere.ai/compatibility/v1", "docsURL": "https://docs.cohere.com/docs/compatibility-api", "category": "Direct APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "command-a-03-2025", "command-r7b-12-2024" ], "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://api.cohere.ai/compatibility/v1" } ] }, { "id": "groq", "label": "Groq", "baseURL": "https://api.groq.com/openai/v1", "docsURL": "https://console.groq.com/docs/openai", "category": "Inference platforms", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "llama-3.3-70b-versatile", "openai/gpt-oss-120b" ], "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://api.groq.com/openai/v1" } ] }, { "id": "together", "label": "Together AI", "baseURL": "https://api.together.xyz/v1", "docsURL": "https://docs.together.ai/", "category": "Inference platforms", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "meta-llama/Llama-3.3-70B-Instruct-Turbo" ], "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://api.together.xyz/v1" } ] }, { "id": "fireworks", "label": "Fireworks AI", "baseURL": "https://api.fireworks.ai/inference/v1", "docsURL": "https://docs.fireworks.ai/", "category": "Inference platforms", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "accounts/fireworks/models/llama-v3p3-70b-instruct" ], "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://api.fireworks.ai/inference/v1" } ] }, { "id": "perplexity", "label": "Perplexity", "baseURL": "https://api.perplexity.ai", "docsURL": "https://docs.perplexity.ai/", "category": "Direct APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "sonar-pro", "sonar" ], "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://api.perplexity.ai" } ] }, { "id": "openrouter", "label": "OpenRouter", "baseURL": "https://openrouter.ai/api/v1", "docsURL": "https://openrouter.ai/docs/quickstart", "category": "Inference platforms", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "anthropic/claude-sonnet-4.5", "openai/gpt-4.1-mini" ], "notes": "The selected aggregator may route to downstream providers according to your account policy. Review its privacy and routing settings.", "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://openrouter.ai/api/v1" } ] }, { "id": "moonshot", "label": "Moonshot (Kimi)", "baseURL": "https://api.moonshot.ai/v1", "docsURL": "https://platform.moonshot.ai/docs/", "category": "Regional APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "kimi-k2-0905-preview", "kimi-k2-turbo-preview" ], "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "International", "url": "https://api.moonshot.ai/v1" }, { "label": "China mainland", "url": "https://api.moonshot.cn/v1" } ] }, { "id": "dashscope", "label": "Alibaba DashScope (Qwen)", "baseURL": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "docsURL": "https://www.alibabacloud.com/help/en/model-studio/base-url", "category": "Regional APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "qwen-plus", "qwen-max", "qwen-turbo" ], "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "International", "url": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1" }, { "label": "China mainland", "url": "https://dashscope.aliyuncs.com/compatible-mode/v1" }, { "label": "US", "url": "https://dashscope-us.aliyuncs.com/compatible-mode/v1" }, { "label": "Hong Kong", "url": "https://cn-hongkong.dashscope.aliyuncs.com/compatible-mode/v1" } ] }, { "id": "ark", "label": "Volcengine Ark (Doubao)", "baseURL": "https://ark.cn-beijing.volces.com/api/v3", "docsURL": "https://www.volcengine.com/docs/82379", "category": "Regional APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "doubao-seed-1-6-250615", "doubao-seed-1-6-flash-250615" ], "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://ark.cn-beijing.volces.com/api/v3" } ] }, { "id": "qianfan", "label": "Baidu Qianfan (ERNIE)", "baseURL": "https://qianfan.baidubce.com/v2", "docsURL": "https://cloud.baidu.com/doc/qianfan/", "category": "Regional APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "ernie-4.5-turbo-128k" ], "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://qianfan.baidubce.com/v2" } ] }, { "id": "hunyuan", "label": "Tencent Hunyuan", "baseURL": "https://api.hunyuan.cloud.tencent.com/v1", "docsURL": "https://cloud.tencent.com/document/product/1729", "category": "Regional APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "hunyuan-turbos-latest", "hunyuan-lite" ], "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://api.hunyuan.cloud.tencent.com/v1" } ] }, { "id": "spark", "label": "iFlytek Spark", "baseURL": "https://spark-api-open.xf-yun.com/v1", "docsURL": "https://www.xfyun.cn/doc/spark/Web.html", "category": "Regional APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "4.0Ultra", "generalv3.5", "lite" ], "notes": "Use the provider’s APIKey:APISecret bearer credential.", "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://spark-api-open.xf-yun.com/v1" } ] }, { "id": "minimax", "label": "MiniMax", "baseURL": "https://api.minimax.io/v1", "docsURL": "https://platform.minimax.io/docs/", "category": "Regional APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "MiniMax-M2", "MiniMax-M1" ], "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "International", "url": "https://api.minimax.io/v1" }, { "label": "China mainland", "url": "https://api.minimaxi.com/v1" } ] }, { "id": "stepfun", "label": "StepFun", "baseURL": "https://api.stepfun.com/v1", "docsURL": "https://platform.stepfun.com/docs/", "category": "Regional APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "step-2-16k", "step-2-mini" ], "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://api.stepfun.com/v1" } ] }, { "id": "baichuan", "label": "Baichuan", "baseURL": "https://api.baichuan-ai.com/v1", "docsURL": "https://platform.baichuan-ai.com/docs", "category": "Regional APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "Baichuan4-Turbo", "Baichuan4-Air" ], "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://api.baichuan-ai.com/v1" } ] }, { "id": "yi", "label": "01.AI (Yi)", "baseURL": "https://api.lingyiwanwu.com/v1", "docsURL": "https://platform.lingyiwanwu.com/docs", "category": "Regional APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "yi-lightning" ], "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://api.lingyiwanwu.com/v1" } ] }, { "id": "sensenova", "label": "SenseTime SenseNova", "baseURL": "https://api.sensenova.cn/compatible-mode/v1", "docsURL": "https://platform.sensenova.cn/doc", "category": "Regional APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "SenseChat-5", "SenseChat-Turbo" ], "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://api.sensenova.cn/compatible-mode/v1" } ] }, { "id": "siliconflow", "label": "SiliconFlow", "baseURL": "https://api.siliconflow.cn/v1", "docsURL": "https://docs.siliconflow.com/", "category": "Regional APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "suggestions": [ "Qwen/Qwen3-235B-A22B-Instruct-2507", "deepseek-ai/DeepSeek-V3", "moonshotai/Kimi-K2-Instruct" ], "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "China mainland", "url": "https://api.siliconflow.cn/v1" }, { "label": "International", "url": "https://api.siliconflow.com/v1" } ] }, { "id": "cerebras", "label": "Cerebras", "baseURL": "https://api.cerebras.ai/v1", "docsURL": "https://inference-docs.cerebras.ai/resources/openai", "category": "Inference platforms", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://api.cerebras.ai/v1" } ] }, { "id": "deepinfra", "label": "DeepInfra", "baseURL": "https://api.deepinfra.com/v1/openai", "docsURL": "https://docs.deepinfra.com/chat/overview", "category": "Inference platforms", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://api.deepinfra.com/v1/openai" } ] }, { "id": "sambanova", "label": "SambaNova", "baseURL": "https://api.sambanova.ai/v1", "docsURL": "https://docs.sambanova.ai/docs/en/get-started/quickstart", "category": "Inference platforms", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://api.sambanova.ai/v1" } ] }, { "id": "nvidia", "label": "NVIDIA NIM / API Catalog", "baseURL": "https://integrate.api.nvidia.com/v1", "docsURL": "https://docs.api.nvidia.com/nim/reference/llm-apis", "category": "Inference platforms", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://integrate.api.nvidia.com/v1" } ] }, { "id": "huggingface", "label": "Hugging Face Inference Providers", "baseURL": "https://router.huggingface.co/v1", "docsURL": "https://huggingface.co/docs/inference-providers/en/tasks/chat-completion", "category": "Inference platforms", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "notes": "Use a token with Inference Providers permission. Downstream provider routing follows your Hugging Face account configuration.", "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://router.huggingface.co/v1" } ] }, { "id": "ai21", "label": "AI21 Labs / Jamba", "baseURL": "https://api.ai21.com/studio/v1", "docsURL": "https://docs.ai21.com/docs/jamba-foundation-models", "category": "Direct APIs", "protocol": "OpenAI-compatible", "access": "direct", "discovery": false, "configurationCapabilities": [ "text" ], "endpoints": [ { "label": "Default API", "url": "https://api.ai21.com/studio/v1" } ] }, { "id": "cloudflare", "label": "Cloudflare Workers AI", "docsURL": "https://developers.cloudflare.com/workers-ai/configuration/open-ai-compatibility/", "category": "Inference platforms", "protocol": "OpenAI-compatible", "access": "direct", "discovery": false, "customEndpoint": true, "notes": "Use https://api.cloudflare.com/client/v4/accounts/YOUR_ACCOUNT_ID/ai/v1 and a Workers AI API token.", "configurationCapabilities": [ "text" ], "endpoints": [] }, { "id": "bedrock", "label": "Amazon Bedrock", "docsURL": "https://docs.aws.amazon.com/bedrock/", "category": "Enterprise gateways", "protocol": "OpenAI-compatible", "access": "gateway", "discovery": true, "customEndpoint": true, "notes": "Requires your institution’s OpenAI-compatible HTTPS gateway and gateway bearer token. Native cloud IAM, OAuth, SigV4 and service-account credentials are not accepted here.", "configurationCapabilities": [ "text" ], "endpoints": [] }, { "id": "vertex", "label": "Google Vertex AI", "docsURL": "https://cloud.google.com/vertex-ai/generative-ai/docs", "category": "Enterprise gateways", "protocol": "OpenAI-compatible", "access": "gateway", "discovery": true, "customEndpoint": true, "notes": "Requires your institution’s OpenAI-compatible HTTPS gateway and gateway bearer token. Native cloud IAM, OAuth, SigV4 and service-account credentials are not accepted here.", "configurationCapabilities": [ "text" ], "endpoints": [] }, { "id": "watsonx", "label": "IBM watsonx.ai", "docsURL": "https://www.ibm.com/docs/en/watsonx-as-a-service", "category": "Enterprise gateways", "protocol": "OpenAI-compatible", "access": "gateway", "discovery": true, "customEndpoint": true, "notes": "Requires your institution’s OpenAI-compatible HTTPS gateway and gateway bearer token. Native cloud IAM, OAuth, SigV4 and service-account credentials are not accepted here.", "configurationCapabilities": [ "text" ], "endpoints": [] }, { "id": "sagemaker", "label": "Amazon SageMaker", "docsURL": "https://docs.aws.amazon.com/sagemaker/", "category": "Enterprise gateways", "protocol": "OpenAI-compatible", "access": "gateway", "discovery": true, "customEndpoint": true, "notes": "Requires your institution’s OpenAI-compatible HTTPS gateway and gateway bearer token. Native cloud IAM, OAuth, SigV4 and service-account credentials are not accepted here.", "configurationCapabilities": [ "text" ], "endpoints": [] }, { "id": "foundry", "label": "Microsoft Foundry / Azure AI", "docsURL": "https://learn.microsoft.com/azure/ai-foundry/", "category": "Enterprise gateways", "protocol": "OpenAI-compatible", "access": "gateway", "discovery": true, "customEndpoint": true, "notes": "Requires your institution’s OpenAI-compatible HTTPS gateway and gateway bearer token. Native cloud IAM, OAuth, SigV4 and service-account credentials are not accepted here.", "configurationCapabilities": [ "text" ], "endpoints": [] }, { "id": "databricks", "label": "Databricks Model Serving", "docsURL": "https://docs.databricks.com/en/machine-learning/model-serving/", "category": "Inference platforms", "protocol": "OpenAI-compatible", "access": "direct", "discovery": false, "customEndpoint": true, "notes": "Use the OpenAI-compatible serving base URL for your workspace and an authorized bearer token.", "configurationCapabilities": [ "text" ], "endpoints": [] }, { "id": "custom", "label": "Other commercial vendor / custom gateway", "category": "Custom endpoints", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "customEndpoint": true, "notes": "For an unlisted vendor, enter its OpenAI-compatible API base URL. This supports chat-completions bearer-token APIs, not every proprietary API protocol.", "configurationCapabilities": [ "text" ], "endpoints": [] }, { "id": "local", "label": "Local LLM: Ollama, LM Studio, vLLM, llama.cpp", "docsURL": "https://docs.ollama.com/api/openai-compatibility", "category": "Local models", "protocol": "OpenAI-compatible", "access": "direct", "discovery": true, "customEndpoint": true, "notes": "Use an authenticated public HTTPS tunnel to your runtime. The cloud proxy cannot connect to localhost or LAN addresses. This is local compute, not an offline connection.", "configurationCapabilities": [ "text" ], "endpoints": [] } ], "catalogCaveat": "Templates do not guarantee model access. Enterprise gateway templates are not native IAM integrations.", "wizardSteps": [ "Choose funding", "Choose provider and region", "Configure endpoint and credentials in the gateway", "Discover models or enter a model/deployment ID", "Review and save", "Test connection", "Authorize the consuming app" ], "savedConnectionActions": [ "Select", "Edit", "Test connection", "Remove" ], "selectionRules": [ "Only use connections authorized for the app", "Omit connection to use the app grant default", "PUT /v1/selection saves the authorized connection per user, app and capability across sign-ins; relink to change permissions", "Never silently fall back to host credentials" ], "testNotice": "Tests make a real provider request and may incur charges." }, "sharedUI": { "module": "https://oauth.skoonline.org/genaiInstruction/ui/v1/services.js", "exports": [ "mountAccountLauncher", "mountAccountSettings", "mountGenAISettings", "mountGoogleSignIn", "mountSMTPSettings", "mountServiceSettings" ], "authentication": "Host backend adapters; never expose service keys, provider keys or access tokens to the component" }, "capabilities": [ { "id": "text", "label": "Text / LLM", "fields": [ "temperature", "maxTokens" ] }, { "id": "image", "label": "Images", "fields": [ "size", "quality", "format" ] }, { "id": "speech", "label": "Voice / text to speech", "fields": [ "voice", "language", "speed", "format" ] }, { "id": "transcription", "label": "Speech to text", "fields": [ "language", "format" ] }, { "id": "realtime", "label": "Realtime voice", "fields": [ "voice", "language" ] }, { "id": "video", "label": "Video", "fields": [ "size", "duration", "format" ] }, { "id": "embeddings", "label": "Embeddings", "fields": [ "dimensions" ] } ], "executionSupport": "Direct personal text execution is the default SDK path. Hosted execution remains on the compatibility proxy pending scoped delegation. Text chat only. Other capability configurations and selections are stored; generation adapters are not yet implemented.", "endpoints": [ { "method": "POST", "url": "https://oauth.skoonline.org/genai/v1/execution-session", "authentication": "User-grant Bearer plus confidential app Basic; explicit direct-execution consent", "purpose": "Backend-only, short-lived personal text configuration handoff; no inference" }, { "method": "PUT", "url": "https://oauth.skoonline.org/genai/v1/selection", "authentication": "GenAI user-grant Bearer token plus X-Client-Authorization Basic client credentials", "purpose": "Persist {connection, capability} per user and app; capability defaults to text" }, { "method": "GET", "url": "https://oauth.skoonline.org/genai/connect", "purpose": "User consent with state and PKCE S256" }, { "method": "POST", "url": "https://oauth.skoonline.org/genai/oauth/token", "authentication": "Basic app credentials", "purpose": "Code exchange or rotating refresh" }, { "method": "GET", "url": "https://oauth.skoonline.org/genai/v1/connections", "authentication": "GenAI user-grant Bearer token plus X-Client-Authorization Basic client credentials" }, { "method": "POST", "url": "https://oauth.skoonline.org/genai/v1/chat/completions", "authentication": "GenAI user-grant Bearer token plus X-Client-Authorization Basic client credentials" }, { "method": "GET", "url": "https://oauth.skoonline.org/genai/v1/operations/:requestId", "authentication": "GenAI user-grant Bearer token plus X-Client-Authorization Basic client credentials" } ], "sections": [ { "id": "overview", "title": "1. Keep your existing login", "text": [ "GenAI is an optional addition to the existing OAuth and SMTP gateway. Keep your app’s current login, user sessions, roles, email relay and OAuth callbacks. Add a “Connect GenAI Management” action in profile settings; linking AI access is separate from signing in.", "Direct execution is the standard for personal text connections: obtain an explicitly consented backend-only execution session, cache it for up to 15 minutes, and call the provider from your app backend. The legacy proxy inference endpoint remains available for compatibility and centrally funded hosted accounts. Keys never reach the browser.", "This guide describes the implemented v1 contract, not a generic OpenAI drop-in API. The service accepts messages, connection, maxTokens, requestId and stream. Its response and SSE events use the shapes below." ] }, { "id": "origin", "title": "2. Use the deployed gateway origin", "text": [ "This deployment's GenAI origin: https://oauth.skoonline.org. Use this origin consistently for linking, management, token exchange and inference.", "These documentation links follow the domain serving this page. The shared UI derives its assets and portal links from its own module URL. Consuming apps receive a trusted proxy origin from their backend configuration, not their own browser origin. Begin and finish login on the same domain.", "Set BASE_URL to the deployed origin; GENAI_PUBLIC_ORIGIN optionally overrides it. Register the matching Google callback. The legacy account namespace is preserved independently of the routing origin; change GENAI_ACCOUNT_NAMESPACE only with a planned data migration." ] }, { "id": "register", "title": "3. Register the consuming application", "text": [ "A gateway administrator opens https://oauth.skoonline.org/genai/, then Administration → Register an application. Choose a stable app ID (for example vhs), a display name, and exact HTTPS callback URLs owned by that app.", "Save the one-time app secret in your app backend’s secret configuration. Callback URLs must have no query string or fragment. Register production and staging explicitly. Never put the app secret in frontend JavaScript, localStorage, a URL, or a Git commit.", "There are two callback settings: the consuming app callback is registered in GenAI Administration. The gateway’s own /genai/auth/callback must be registered in Google Cloud for its direct Google sign-in. Register consuming app callbacks separately under OAuth and GenAI in Administration. Select the services the client may use." ], "code": "GENAI_ORIGIN=https://oauth.skoonline.org\nPROXY_CLIENT_ID=your-registered-app-id\nPROXY_CLIENT_SECRET=\nGENAI_CALLBACK_URL=https://yourapp.example/auth/genai/callback" }, { "id": "optional-connections", "title": "Optional app storage and three xAPI LRS destinations", "text": [ "Register optional GCS and primary/debug/webhooks LRS settings at oauth.skoonline.org. In each app set USE_OAUTH_GCS=true to use registered storage and USE_OAUTH_LRS=true to use registered LRS profiles. These switches are independent; false or unset uses that app's original configuration. The backend calls POST /genai/services/config with Basic PROXY_CLIENT_ID:PROXY_CLIENT_SECRET and JSON {gcs:true,lrs:true} matching enabled switches. Use loadOptionalConnections from genai/sdk/optional-connections.js at server startup; it retrieves only requested settings. A complete gateway profile takes precedence, otherwise the complete original profile is the fallback. Never mix profiles. Network errors, HTTP 429 and 5xx use original configuration; 4xx authentication/configuration errors are surfaced. This selects destinations at initialization; it does not migrate GCS data or perform per-operation storage failover. Keep credentials and responses server-side.", "In Connect a new app or App settings, expand Optional storage and xAPI LRS connections. All four connections and every field are optional. Each LRS has URL, User, and Secret / Password. Leave unused settings blank.", "These settings are per-app deployment configuration. Set USE_OAUTH_GCS and USE_OAUTH_LRS independently on the app server, then load the registered configuration with the SDK helper. The app backend retrieves the requested settings from POST /genai/services/config using its existing client credentials; OAuth tokens and GenAI grants do not include these settings.", "Use the app runtime Google Cloud service account/Application Default Credentials with @google-cloud/storage. GCS_BUCKET is a bucket name, not gs:// or a URL. Grant appropriate private bucket permissions separately. An unset bucket leaves existing storage behavior unchanged; never silently replace durable storage with memory.", "Read each LRS independently on the app backend. An empty or partial gateway profile falls back to the complete original profile. If neither is complete, keep it inactive. Construct Basic authentication from base64(User + \":\" + Secret/Password). Append statements to the configured xAPI base URL after normalizing its trailing slash. Send Content-Type: application/json and X-Experience-API-Version: 1.0.3 for xAPI 1.0.3 statements. Use valid actor, verb and object; obtain learner identity from a verified app session.", "Primary receives intended learning/assessment statements; debug receives explicitly enabled diagnostics with minimized data; webhooks receives statements for verified webhook events. Do not fan out automatically or fall back between profiles. The webhook LRS is an outbound destination, not an incoming webhook registration; authenticate incoming webhooks in the receiving app.", "Generate and persist a statement UUID before delivery. For retry-safe xAPI 1.0.3 delivery, PUT /statements?statementId= with the same statement. Use bounded timeouts and an app-owned durable outbox for retries; classify failures and avoid blocking the learning activity. Do not claim delivery until HTTP success.", "Never expose LRS credentials in browser bundles, public JSON, URLs, logs or commits. The administrator-only app detail route decrypts saved values for editing; lists and usage omit them. Copy into server secret configuration. Existing config updates omitting optionalConnections preserve values; explicit blanks clear fields." ], "code": "OAUTH_BASE_URL=https://oauth.skoonline.org\nPROXY_CLIENT_ID=your-app-id\nPROXY_CLIENT_SECRET=\nUSE_OAUTH_GCS=true\nUSE_OAUTH_LRS=true\n# Keep original app GCS/LRS values for fallback.\nconst { loadOptionalConnections } = require('./optional-connections');\nconst connections = await loadOptionalConnections(process.env);" }, { "id": "user", "title": "4. Let the user prepare and test a connection", "text": [ "In the GenAI portal, the user selects a provider/region, enters a model or Azure deployment name, saves their API key, and clicks Test connection. Existing connections can be edited, reused and removed. Tests make a small real provider request and can incur provider charges.", "Personal provider: the vendor bills the user’s provider account directly. Local model: use an authenticated public HTTPS endpoint to Ollama, LM Studio, vLLM or another compatible runtime. A cloud proxy cannot reach the user’s localhost or private LAN.", "Hosted model: available only when the operator configures it. Shared allowance or credit funds requests. This version has manual verified-credit administration, not online checkout. Administrators follow the same funding rules.", "The 41 templates include direct APIs and compatible gateways. Bedrock, Vertex, Foundry, SageMaker and watsonx templates require a compatible gateway; they are not native IAM adapters. A catalog entry is not proof of current model access. Discover IDs from a saved connection or enter them manually, then test." ] }, { "id": "sdk", "title": "5. Install the server-side client", "text": [ "Download the SDK using the link at the top of this page and save it as genai-client.js in the consuming app backend. It is a CommonJS module for Node.js 22 or later. Download direct-execution.js alongside genai-client.js and install ipaddr.js for the DNS-pinned HTTPS provider transport. No npm package publication is required.", "Pass only the origin, without /genai; the SDK adds the prefix. Keep the downloaded version pinned with your app and review updates before replacing it. Upgrade the SDK before enabling common-client authentication on an existing registration. Its requests include the app secret server-side in X-Client-Authorization, in addition to user-grant tokens." ], "code": "const { GenAIClient } = require('./genai-client');\nconst client = new GenAIClient({\n origin: 'https://oauth.skoonline.org',\n clientId: process.env.PROXY_CLIENT_ID,\n clientSecret: process.env.PROXY_CLIENT_SECRET,\n});" }, { "id": "link", "title": "6. Link through explicit user consent", "text": [ "Run linking only from an already-authenticated app session. Protect the start action using the app’s existing CSRF mechanism. Generate the URL, state and PKCE verifier with client.link(callbackURL). Persist the transaction server-side under the current app user and session before redirecting.", "The portal asks which connections the app may use and which is its default. Logging in alone never authorizes API-key use or spending. Existing gateway tokens and suite identity tokens are not GenAI inference tokens.", "At the app callback, require the same authenticated app session; check the saved owner, expiry and returned state, and atomically consume the transaction once. Reject mismatches before exchanging the code. Do not trust a callback email or user ID. Use a dedicated GenAI callback; leave the existing OAuth callback unchanged.", "After exchange, encrypt and persist the returned access/refresh tokens under that app user. Never expose them in frontend code or logs. The transaction store and token store below are integration adapters your app must supply; the fragments are not a complete session or persistence implementation." ], "code": "// Start: after existing app authentication and CSRF checks.\nconst link = client.link(process.env.GENAI_CALLBACK_URL);\nawait linkStore.save({\n owner: currentUser.id, sessionId: currentSession.id,\n state: link.state, verifier: link.verifier,\n expiresAt: Date.now() + 10 * 60 * 1000,\n});\nres.redirect(link.url);\n\n// Callback: consumeForSession must verify owner, session, state, expiry\n// and atomically reject replay. It must throw on any mismatch.\nconst pending = await linkStore.consumeForSession({\n owner: currentUser.id, sessionId: currentSession.id,\n returnedState: req.query.state,\n});\nconst tokens = await client.exchange({\n code: req.query.code,\n redirectUri: process.env.GENAI_CALLBACK_URL,\n verifier: pending.verifier,\n});\nawait tokenStore.saveEncrypted(currentUser.id, tokens);\nres.redirect('/profile');" }, { "id": "direct-execution", "title": "Direct provider execution standard", "text": [ "POST /genai/v1/execution-session accepts {connection?}. Require the user-grant Bearer token and confidential app credentials in X-Client-Authorization. Browser-origin requests are rejected. The user must explicitly enable direct execution when granting connections; existing grants do not authorize credential handoff.", "The response contains one approved personal text connection: provider, model, endpoint, apiVersion, apiKey and maxTokens, plus appId, revision and expiresAt. Keep this response in server memory only, scoped to the verified local user and session. Never expose it through a browser settings endpoint, logging or an error body.", "Reuse the session across agents and HTTP turns until expiresAt, capped at 15 minutes and the access/grant expiry. Coalesce concurrent loads. Invalidate on app selection/routing changes, reconnect, disconnect and logout. Reload after expiry; do not reload for each agent. Other instances can retain their cache until expiry.", "The SDK defaults to direct execution. Download /genaiInstruction/direct-execution.js beside the main SDK. Its transport pins public-address validation to socket lookup and rejects redirects. Applications with existing adapters may supply providerFetch(endpoint), with equivalent DNS and origin protections.", "Generation requests go directly from the application backend to the provider. The gateway does not apply its inference admission queue or central budget ledger to those requests. Applications must durably attribute usage, manage concurrency and duplicate-request protection, and preserve incomplete-outcome handling. A provider interruption is never an instruction to retry automatically.", "A cache expiry is not a provider credential expiry. Revocation blocks new configuration handoffs; only provider-side key revocation or rotation invalidates a key already handed to an app. Shared hosted credentials are not exported: use a personal connection or implement provider-scoped delegated credentials and budgets before migrating hosted execution.", "This text-only contract does not migrate media generation, OAuth sign-in, SMTP delivery or optional GCS/LRS settings. Existing integrations require individual adapter upgrades; publishing this contract alone does not change them." ], "code": "const client = new GenAIClient({origin: 'https://oauth.skoonline.org', clientId, clientSecret});\nconst result = await client.complete(accessToken, {messages, maxTokens: 'max'});\n// Reuse this server-side client; write measured/uncertain usage to the app datastore.\n// Explicit compatibility only: new GenAIClient({...options, executionMode: 'proxy'})." }, { "id": "inference", "title": "7. Legacy proxy request reference", "text": [ "Obtain the user’s server-stored access token and list authorized connections with client.connections(accessToken). Omit connection to use the app grant’s default, or send an approved connection ID. Changing the account-wide default does not change an existing app grant. Relink to change the app’s permissions/default, then revoke its old grant.", "Generate and durably persist the request ID before sending. The ID starts with the current UTC YYYY-MM followed by underscore and a unique suffix. Keep it with your app’s operation record for diagnosis.", "Messages support system, user and assistant roles with text content: 1–100 messages, at most 100,000 characters in their serialized array. maxTokens must be an integer from 1 to 8192. Images, audio, tool calls, embeddings and arbitrary vendor parameters are not exposed by this version.", "The gateway does not persist prompts or generated text in its usage ledger. Your app decides whether to store the returned response under its own privacy and retention policy." ], "code": "const requestId = client.requestId();\nawait operationStore.create({ owner: currentUser.id, requestId });\nconst result = await client.complete(accessToken, {\n requestId,\n messages: [{ role: 'user', content: 'Explain this experiment.' }],\n maxTokens: 1024,\n // connection: approvedConnectionId, // optional\n});\n// result: {content, usage: {input, output} | null, requestId,\n// connection, model, state: 'completed' | 'uncertain'}\nawait operationStore.recordResult(requestId, result);" }, { "id": "stream", "title": "8. Streaming and legacy operation lookup", "text": [ "client.stream sends stream:true and parses named SSE events. delta carries {text}; done carries usage, requestId, connection, model and state. error carries a safe code/message. Forward text to your frontend using your app’s existing transport.", "A stream ending without done is not proof that inference failed or was free. Missing provider usage produces state:uncertain. Inspect the operation before deciding what to do. A disconnected client may still leave a provider request running." ], "code": "await client.stream(accessToken, {\n requestId, // already persisted; do not reuse an ID from another operation\n messages: [{ role: 'user', content: 'Explain this experiment.' }],\n maxTokens: 1024,\n}, async (event, data) => {\n if (event === 'delta') sendTextToBrowser(data.text);\n if (event === 'done') await operationStore.recordMetadata(requestId, data);\n // The SDK throws on error events or a stream without a final done event.\n});\n\n// After a network interruption or duplicate-request response:\nconst status = await client.operation(accessToken, requestId);" }, { "id": "tokens", "title": "9. Legacy proxy accounting and token lifecycle", "text": [ "Access tokens last 15 minutes. Refresh tokens rotate on use and expire no later than 30 days or the grant expiry. Serialize refreshes per app user across backend instances and atomically replace stored tokens. A lost exchange/refresh response can require relinking.", "Authorization codes are one-use, app-bound, redirect-bound and PKCE-bound, with a two-minute expiry. App grants last 30 days. Users can revoke access in Applications; admins can disable an app. The next request checks the latest grant/app state. Revocation does not cancel already-running requests.", "A used request ID returns HTTP 409; it does not replay the response. Do not silently retry inference with a new ID or switch to a host key. Check operation status, explain uncertainty, and require an explicit user decision if another generation is needed.", "Previous-month request IDs are rejected for new inference. Older settled operation details can be compacted by the current ledger; keep required audit records in the consuming app. Pending and uncertain reservations remain held until verified reconciliation." ], "code": "// Inside a distributed per-user refresh lock:\nconst nextTokens = await client.refresh(savedTokens.refresh_token);\nawait tokenStore.replaceEncryptedAtomically(currentUser.id, nextTokens);" }, { "id": "api", "title": "10. HTTP endpoint reference", "text": [ "All paths below use the configured origin. Browser management APIs use the GenAI session plus same-origin protections. App inference APIs use GenAI user-grant Bearer token plus X-Client-Authorization Basic client credentialss, never gateway login JWTs.", "Basic app credentials are base64(clientId + \":\" + clientSecret). They identify the app at token exchange; credentials alone cannot select an arbitrary user or access personal keys." ], "code": "GET /genai/ Portal\nGET /genai/connect Consent (client_id, redirect_uri, state,\n code_challenge, code_challenge_method=S256)\nPOST /genai/oauth/token Basic app credentials; JSON body\nGET /genai/v1/connections Bearer user-grant access token\nPOST /genai/v1/chat/completions Bearer user-grant access token; JSON body\nGET /genai/v1/operations/:requestId Bearer user-grant access token\n\n// Authorization-code exchange JSON:\n{ \"grant_type\": \"authorization_code\", \"code\": \"\",\n \"redirect_uri\": \"https://yourapp.example/auth/genai/callback\",\n \"code_verifier\": \"\" }\n\n// Refresh JSON:\n{ \"grant_type\": \"refresh_token\", \"refresh_token\": \"\" }" }, { "id": "errors", "title": "11. Diagnostics and recovery", "text": [ "401 access_expired: refresh once through the serialized backend refresh path. Invalid refresh or revoked grant: reconnect. 403 grant_scope: choose an approved connection or relink.", "402 credit_required: the hosted allowance/credit cannot fund the reservation. Offer a personal provider or administrator-assisted credit. Do not bypass funding for admins.", "409 duplicate_request or request_id_conflict: inspect the original operation. 409 choose_connection or hosted_unavailable: return the user to connection settings.", "429 pending_limit or monthly_request_limit: inspect unresolved operations or the initial-release capacity limits. 503 inference_busy: no provider work was started by that rejected request; communicate capacity to the user.", "provider_auth, provider_model, provider_limit and provider_unavailable describe upstream key/access, endpoint/model, quota/rate, or network/provider failures. Use portal Test connection. Do not display raw vendor error bodies or credentials.", "The portal may show genai_setup or an enablement notice until the administrator configures the module. Availability of this public instruction page does not prove inference is enabled." ] }, { "id": "deployment", "title": "12. Operator setup and release acceptance", "text": [ "Enable GENAI_ENABLED=true; set BASE_URL to the deployed HTTPS origin, STORAGE_TYPE=gcs and GCS_BUCKET_NAME to your private shared bucket. Leave GENAI_GCS_BUCKET unset to inherit that bucket. GENAI_PUBLIC_ORIGIN optionally overrides BASE_URL. Keep API_KEY_ENCRYPTION_SECRET stable (at least 32 bytes) and grant bucket access to the runtime service identity. GenAI uses the separate genai/v1/ namespace.", "Preserve existing OAuth/SMTP environment values and CORS configuration. Register exact consuming-app redirects in REDIRECT_URI_ALLOWLIST and GenAI app callbacks separately in GenAI Administration. Register the gateway Google callbacks in the Google OAuth client. Verified admin email configuration is separate from app-user roles.", "HOSTED_MODELS_JSON=[] enables personal/local usage without host keys. Hosted models require separately configured host credentials, operator-approved tariffs and allowances. Do not copy example prices as current vendor prices.", "Acceptance checks: existing app login and SMTP still work; linking rejects wrong/replayed state; keys never reach the browser; direct keys reach only the consenting user’s authorized backend; saved connection test succeeds; non-authorized connections are rejected; streamed requests finish with usage or explicit uncertainty; revocation works; refreshes serialize; duplicate IDs never trigger a second generation.", "Initial limits: 20 saved connections, 100 grants and 2,000 operations per account/month, five pending/uncertain calls, plus per-instance AI admission. This GCS-backed release is not certified for millions of active users. Normalize the operational datastore and perform production-like load testing before mass rollout.", "Teacher/class sponsorship, online credit checkout, xAPI export and native enterprise IAM are not implemented in this addition. Keep these as explicit future work rather than inferring support from the provider templates." ] }, { "id": "agent", "title": "13. Handoff for an AI coding assistant", "text": [ "Give the assistant the plain-text version of this page and the SDK. Ask it to integrate only the GenAI connection flow and request adapter, preserving your existing login and email behavior." ], "code": "Integrate the SKO GenAI gateway at https://oauth.skoonline.org.\nRead https://oauth.skoonline.org/genaiInstruction.txt and use the server-only SDK at\nhttps://oauth.skoonline.org/genaiInstruction/sdk.js.\nPreserve existing OAuth login, user identities, permissions and SMTP behavior.\nAdd Connect/Manage AI to profile settings with explicit per-app user consent.\nUse backend-only execution sessions and direct provider calls for personal text.\nDownload direct-execution.js beside the SDK and install ipaddr.js.\nKeep app credentials and user tokens encrypted on the backend.\nImplement one-use state/PKCE linking, serialized token refresh, scoped connection\nselection, durable request IDs, streaming and uncertain-outcome handling.\nDo not use a gateway login JWT as a GenAI access token. Do not silently fall back\nto a host key, retry inference with a new ID, or invent unsupported API features.\nUse the app's durable session/token stores and include targeted integration tests.\nReport runtime configuration still required; do not claim deployment from a commit." } ], "availability": "Public documentation does not prove GenAI is enabled or deployed with working credentials." }, "verification": [ "Check wrong/rotated client secrets, disabled clients and disabled service scopes are rejected for all three services.", "Open the guide on the deployed domain; links and module assets must use that domain.", "Google login returns to the registered callback, restores the correct user and local role, and rejects reused or mismatched state.", "Verify SMTP only with an explicitly requested test message; confirm receipt and check failures without exposing tokens.", "Save provider credentials in the portal; authorize the consuming app; select a model; refresh, sign out/in and restart the app to verify persistence.", "Ensure revoked grants, cross-user access and unauthorized model IDs are rejected; no silent fallback to a different provider.", "Verify all services use the intended GCS bucket with separate prefixes; never make the bucket public.", "Non-text AI configurations can be saved; current proxy execution supports text chat only." ] }