+ +
+
+

1. Let's make a test request

+
The gateway supports 250+ models across 36 AI providers. Choose your provider and API + key below.
+
+
🐍 Python
+
📦 Node.js
+
🌀 cURL
+
+
+ +
+
+
+
+
+
+
+
+
+
+ + + +
+
+ +
+

2. Create a routing config

+
Gateway configs allow you to route requests to different providers and models. You can load balance, set fallbacks, and configure automatic retries & timeouts. Learn more
+
+
Simple Config
+
Load Balancing
+
Fallbacks
+
Retries & Timeouts
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + +
+ + +
+
+ + + + +

Setup a Call

+

Get personalized support and learn how Portkey can be tailored to your needs.

+ Schedule Consultation +
+
+ + + + + +

Enterprise Features

+

Explore advanced features and see how Portkey can scale with your business.

+ View Enterprise Plan +
+
+ + + + +

Join Our Community

+

Connect with other developers, share ideas, and get help from the Portkey team.

+ Join Discord +
+
+
+
+ +
+
+

Real-time Logs

+
+ + +
+
+ + + + + + + + + + + + + + + + + + +
TimeMethodEndpointStatusDurationActions
+
+ Listening for logs... +
+
+
+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/public/main.js b/src/public/main.js new file mode 100644 index 000000000..0b5e11077 --- /dev/null +++ b/src/public/main.js @@ -0,0 +1,708 @@ +// function bedrockConfigNode(vars) { +// return `, +// awsAccessKeyId: "${vars.providerDetails?.awsAccessKeyId || '[Click to edit]'}", +// awsSecretAccessKey: "${vars.providerDetails?.awsSecretAccessKey || '[Click to edit]'}", +// awsRegion: "${vars.providerDetails?.awsRegion || '[Click to edit]'}"${vars.providerDetails?.awsSessionToken ? `, +// awsSessionToken: "${vars.providerDetails.awsSessionToken}"` : ''}`; +// } + +// function bedrockConfigPython(vars) { +// return `, +// aws_access_key_id="${vars.providerDetails?.awsAccessKeyId || '[Click to edit]'}", +// aws_secret_access_key="${vars.providerDetails?.awsSecretAccessKey || '[Click to edit]'}", +// aws_region="${vars.providerDetails?.awsRegion || '[Click to edit]'}"${vars.providerDetails?.awsSessionToken ? `, +// aws_session_token="${vars.providerDetails.awsSessionToken}"` : ''}`; +// } + +// function bedrockConfigCurl(vars) { +// return `\n-H "x-portkey-aws-access-key-id: ${vars.providerDetails?.awsAccessKeyId || '[Click to edit]'}" \\ +// -H "x-portkey-aws-secret-access-key: ${vars.providerDetails?.awsSecretAccessKey || '[Click to edit]'}" \\ +// -H "x-portkey-aws-region: ${vars.providerDetails?.awsRegion || '[Click to edit]'}" \\${vars.providerDetails?.awsSessionToken ? `\n-H "x-portkey-aws-session-token: ${vars.providerDetails.awsSessionToken}" \\` : ''}`; +// } + +// function azureConfigCurl(vars) { +// return `\n-H "x-portkey-azure-resource-name: ${vars.providerDetails?.azureResourceName || '[Click to edit]'}" \\ +// -H "x-portkey-azure-deployment-id: ${vars.providerDetails?.azureDeploymentId || '[Click to edit]'}" \\ +// -H "x-portkey-azure-api-version: ${vars.providerDetails?.azureApiVersion || '[Click to edit]'}" \\ +// -H "x-portkey-azure-model-name: ${vars.providerDetails?.azureModelName || '[Click to edit]'}" \\`; +// } + +// function azureConfigNode(vars) { +// return `, +// azureResourceName: "${vars.providerDetails?.azureResourceName || '[Click to edit]'}", +// azureDeploymentId: "${vars.providerDetails?.azureDeploymentId || '[Click to edit]'}", +// azureApiVersion: "${vars.providerDetails?.azureApiVersion || '[Click to edit]'}", +// azureModelName: "${vars.providerDetails?.azureModelName || '[Click to edit]'}"`; +// } + +// function azureConfigPython(vars) { +// return `, +// azure_resource_name="${vars.providerDetails?.azureResourceName || '[Click to edit]'}", +// azure_deployment_id="${vars.providerDetails?.azureDeploymentId || '[Click to edit]'}", +// azure_api_version="${vars.providerDetails?.azureApiVersion || '[Click to edit]'}", +// azure_model_name="${vars.providerDetails?.azureModelName || '[Click to edit]'}"`; +// } + +// Case conversion utilities +function toCamelCase(str) { + return str.replace(/([-_][a-z])/g, group => + group.toUpperCase() + .replace('-', '') + .replace('_', '') + ); +} + +function toSnakeCase(str) { + return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); +} + +function toKebabCase(str) { + return str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`); +} + +// Base configuration fields for each provider +const providerConfigs = { + bedrock: { + fields: [ + 'awsAccessKeyId', + 'awsSecretAccessKey', + 'awsRegion', + 'awsSessionToken' + ] + }, + azure: { + fields: [ + 'azureResourceName', + 'azureDeploymentId', + 'azureApiVersion', + 'azureModelName' + ] + } +}; + +const modelMap = { + "openai": "gpt-4o-mini", + "anthropic": "claude-3-5-sonnet-20240620", + "groq": "llama3-70b-8192", + "bedrock": "anthropic.claude-3-sonnet-20240229-v1:0", + "azure-openai": "gpt-4o-mini", + "cohere": "command-r-plus", + "together-ai": "llama-3.1-8b-instruct", + "perplexity-ai": "pplx-7b-online", + "mistral-ai": "mistral-small-latest", + "others": "gpt-4o-mini" +} + +const docsMap = { + "openai": "https://portkey.ai/docs/integrations/llms/openai", + "anthropic": "https://portkey.ai/docs/integrations/llms/anthropic", + "groq": "https://portkey.ai/docs/integrations/llms/groq", + "bedrock": "https://portkey.ai/docs/integrations/llms/aws-bedrock", + "azure-openai": "https://portkey.ai/docs/integrations/llms/azure-openai", + "cohere": "https://portkey.ai/docs/integrations/llms/cohere", + "together-ai": "https://portkey.ai/docs/integrations/llms/together-ai", + "perplexity-ai": "https://portkey.ai/docs/integrations/llms/perplexity-ai", + "mistral-ai": "https://portkey.ai/docs/integrations/llms/mistral-ai", + "others": "https://portkey.ai/docs/integrations/llms" +} + +// Format generators for different styles +const formatGenerators = { + node: { + formatKey: fieldName => fieldName, + separator: ':', + indent: ' ', + template: (key, separator, value) => + ` ${key}${separator}"${value}"`, + joinWith: ',\n', + prefix: ',\n', + }, + python: { + formatKey: fieldName => toSnakeCase(fieldName), + separator: '=', + indent: ' ', + template: (key, separator, value) => + ` ${key}${separator}"${value}"`, + joinWith: ',\n', + prefix: ',\n', + }, + curl: { + formatKey: fieldName => `x-portkey-${toKebabCase(fieldName)}`, + separator: ':', + indent: '', + template: (key, separator, value) => + `-H "${key}${separator} ${value}" \\`, + joinWith: '\n', + prefix: '\n', + } +}; + +// Helper function to generate highlighted value span +function generateHighlightedValue(value, id) { + const isEmpty = !value; + const displayValue = value || '[Click to edit]'; + return `${displayValue}`; +} + +// Generic config generator function +function generateConfig(provider, format, vars) { + const { fields } = providerConfigs[provider]; + const formatter = formatGenerators[format]; + const details = vars.providerDetails || {}; + + const configLines = fields + .map(fieldName => { + // Skip session token if not provided (for Bedrock) + if (fieldName === 'awsSessionToken' && !details[fieldName]) return null; + + const key = formatter.formatKey(fieldName); + const value = generateHighlightedValue(details[fieldName], fieldName); + return formatter.template(key, formatter.separator, value); + }) + .filter(Boolean) + .join(formatter.joinWith); + + return formatter.prefix + configLines; +} + + +function getTestRequestCodeBlock(language, vars) { + switch (language) { + case 'nodejs': + return ` +import Portkey from 'portkey-ai' + +const portkey = new Portkey({ + provider: "${vars.provider || '[Click to edit]'}"${vars.provider != 'bedrock' ? `, + Authorization: "${vars.providerDetails?.apiKey || '[Click to edit]'}"`: ''}${vars.provider === 'azure-openai' ? `${generateConfig('azure', 'node', vars)}` : ''}${vars.provider === 'bedrock' ? `${generateConfig('bedrock', 'node', vars)}` : ''} +}) + +// Example: Send a chat completion request +const response = await portkey.chat.completions.create({ + messages: [{ role: 'user', content: 'Hello, how are you?' }], + model: "${modelMap[vars.provider] || ''}"${vars.provider=="anthropic"?`, + max_tokens: 40`:''} +}) +console.log(response.choices[0].message.content)`.trim(); + + case 'python': + return ` +from portkey_ai import Portkey + +client = Portkey( + provider="${vars.provider || '[Click to edit]'}"${vars.provider != 'bedrock' ? `, + Authorization="${vars.providerDetails?.apiKey || '[Click to edit]'}"`: ''}${vars.provider === 'azure-openai' ? `${generateConfig('azure', 'python', vars)}` : ''}${vars.provider === 'bedrock' ? `${generateConfig('bedrock', 'python', vars)}` : ''} +) + +# Example: Send a chat completion request +response = client.chat.completions.create( + messages=[{"role": "user", "content": "Hello, how are you?"}], + model="${modelMap[vars.provider] || ''}" +) +print(response.choices[0].message.content)`.trim(); + + case 'curl': + return `curl -X POST \\ +https://api.portkey.ai/v1/chat/completions \\ +-H "Content-Type: application/json" \\ +-H "x-portkey-provider: ${vars.provider || '[Click to edit]'}" \\${vars.provider != 'bedrock' ? ` +-H "Authorization: ${vars.providerDetails?.apiKey || '[Click to edit]'}" \\`: '' }${vars.provider === 'azure-openai' ? `${generateConfig('azure', 'curl', vars)}` : ''}${vars.provider === 'bedrock' ? `${generateConfig('bedrock', 'curl', vars)}` : ''} +-d '{ + "messages": [ + { "role": "user", "content": "Hello, how are you?" }, + ], + "model": ""${modelMap[vars.provider] || ''}"" +}'`.trim(); + } +} + + +function getRoutingConfigCodeBlock(language, type) { + return configs[language][type]; +} + +// Needed for highlight.js +const lngMap = {"nodejs": "js", "python": "py", "curl": "sh"} + +// Initialize Lucide icons +lucide.createIcons(); + +// Variables +let provider = ''; +let apiKey = ''; +let providerDetails = {}; +let logCounter = 0; + +// DOM Elements +const providerValue = document.getElementById('providerValue'); +const apiKeyValue = document.getElementById('apiKeyValue'); +const copyBtn = document.getElementById('copyBtn'); +const testRequestBtn = document.getElementById('testRequestBtn'); +const logsContent = document.getElementById('logsContent'); +const providerDialog = document.getElementById('providerDialog'); +const apiKeyDialog = document.getElementById('apiKeyDialog'); +const providerSelect = document.getElementById('providerSelect'); +const apiKeyInput = document.getElementById('apiKeyInput'); +const saveApiKeyBtn = document.getElementById('saveApiKeyBtn'); +const saveApiDetailsBtn = document.getElementById('saveApiDetailsBtn'); +const languageSelect = document.getElementById('languageSelect'); +const copyConfigBtn = document.getElementById('copyConfigBtn'); + +const camelToSnakeCase = str => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); +const camelToKebabCase = str => str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`); + +// Dummy function for test request +function dummyTestRequest() { + // Make an API request to the Portkey API + // Use the provider and providerDetails to make the request + const myHeaders = new Headers(); + Object.keys(providerDetails).forEach(key => { + if (key === 'apiKey') { + myHeaders.append("Authorization", providerDetails[key]); + } else { + myHeaders.append("x-portkey-" + camelToKebabCase(key), providerDetails[key]); + } + }) + myHeaders.append("Content-Type", "application/json"); + myHeaders.append("x-portkey-provider", provider); + + const raw = JSON.stringify({ + "messages": [{"role": "user","content": "How are you?"}], + "model": modelMap[provider], + "max_tokens": 40 + }); + + const requestOptions = {method: "POST", headers: myHeaders, body: raw}; + + // Add loading class to testRequestBtn + testRequestBtn.classList.add('loading'); + + fetch("/v1/chat/completions", requestOptions) + .then((response) => { + if (!response.ok) { + return response.json().then(error => { + const responseDiv = document.getElementById('testRequestResponse'); + responseDiv.innerHTML = `[${response.status} ${response.statusText}]: ${error.message || error.error.message}`; + responseDiv.style.display = 'block'; + throw new Error(error); + }); + } + return response.json(); + }) + .then((result) => { + const responseDiv = document.getElementById('testRequestResponse'); + responseDiv.innerHTML = `${result.choices[0].message.content}`; + responseDiv.style.display = 'block'; + responseDiv.classList.remove('error'); + }) + .catch((error) => { + console.error('Error:', error); + }) + .finally(() => { + // Remove loading class from testRequestBtn + testRequestBtn.classList.remove('loading'); + }); +} + +// Functions + +function switchTab(tabsContainer, tabName, updateRoutingConfigFlag = true) { + const tabs = tabsContainer.querySelectorAll('.tab'); + const tabContents = tabsContainer.closest('.card').querySelectorAll('.tab-content'); + + tabs.forEach(tab => tab.classList.remove('active')); + tabContents.forEach(content => content.classList.remove('active')); + + tabsContainer.querySelector(`.tab[data-tab="${tabName}"]`).classList.add('active'); + tabsContainer.closest('.card').querySelector(`#${tabName}Content`).classList.add('active'); + + if (tabsContainer.classList.contains('test-request-tabs')) { + updateAllCommands(); + // Update the language select with the active tab + languageSelect.value = tabName; + updateRoutingConfigFlag ? updateRoutingConfig() : null; + } else if (tabsContainer.classList.contains('routing-config-tabs')) { + updateRoutingConfig(); + } +} + +function updateAllCommands() { + ["nodejs", "python", "curl"].forEach(language => { + const command = document.getElementById(`${language}Command`); + const code = getTestRequestCodeBlock(language, {provider, providerDetails}); + command.innerHTML = code; + if (provider) { + const docsLink = document.querySelector('.docs-link'); + docsLink.innerHTML = `View detailed docs for ${provider == "others" ? "all providers" : provider} `; + docsLink.style.display = 'inline-block'; + } + }); + addClickListeners(); +} + +function highlightElement(element) { + element.classList.add('animate-highlight'); + setTimeout(() => element.classList.remove('animate-highlight'), 1000); +} + +function showProviderDialog() { + providerDialog.style.display = 'flex'; +} + +function getProviderFields(provider) { + switch(provider) { + case 'openai': + case 'anthropic': + case 'groq': + return [{ id: 'apiKey', placeholder: 'Enter your API key' }]; + case 'azure-openai': + return [ + { id: 'apiKey', placeholder: 'Enter your API key' }, + { id: 'azureResourceName', placeholder: 'Azure Resource Name' }, + { id: 'azureDeploymentId', placeholder: 'Azure Deployment ID' }, + { id: 'azureApiVersion', placeholder: 'Azure API Version' }, + { id: 'azureModelName', placeholder: 'Azure Model Name' } + ]; + case 'bedrock': + return [ + { id: 'awsAccessKeyId', placeholder: 'AWS Access Key ID' }, + { id: 'awsSecretAccessKey', placeholder: 'AWS Secret Access Key' }, + { id: 'awsRegion', placeholder: 'AWS Region' }, + { id: 'awsSessionToken', placeholder: 'AWS Session Token (optional)' } + ]; + default: + return [{ id: 'apiKey', placeholder: 'Enter your API key' }]; + } +} + +function showApiKeyDialog() { + // apiKeyDialog.style.display = 'flex'; + const form = document.getElementById('apiDetailsForm'); + form.innerHTML = ''; // Clear existing fields + + const fields = getProviderFields(provider); + fields.forEach(field => { + const label = document.createElement('label'); + label.textContent = field.placeholder; + label.for = field.id; + form.appendChild(label); + const input = document.createElement('input'); + // input.type = 'password'; + input.id = field.id; + input.className = 'input'; + // input.placeholder = field.placeholder; + input.value = providerDetails[field.id] || ""; + form.appendChild(input); + }); + + apiKeyDialog.style.display = 'flex'; +} + +function updateRoutingConfig() { + const language = languageSelect.value; + const activeTab = document.querySelector('.routing-config-tabs .tab.active').dataset.tab; + const codeElement = document.getElementById(`${activeTab}Code`); + + // Also change the tabs for test request + switchTab(document.querySelector('.test-request-tabs'), language, false); + + const code = getRoutingConfigCodeBlock(language, activeTab); + codeElement.innerHTML = hljs.highlight(code, {language: lngMap[language]}).value; +} + +function addClickListeners() { + const providerValueSpans = document.querySelectorAll('.highlighted-value:not(#providerValue)'); + const providerValues = document.querySelectorAll('[id^="providerValue"]'); + // const apiKeyValues = document.querySelectorAll('[id^="apiKeyValue"]'); + + providerValues.forEach(el => el.addEventListener('click', showProviderDialog)); + // apiKeyValues.forEach(el => el.addEventListener('click', showApiKeyDialog)); + providerValueSpans.forEach(el => el.addEventListener('click', showApiKeyDialog)); +} + + +// Event Listeners +testRequestBtn.addEventListener('click', dummyTestRequest); + +document.querySelectorAll('.tabs').forEach(tabsContainer => { + tabsContainer.querySelectorAll('.tab').forEach(tab => { + tab.addEventListener('click', () => switchTab(tabsContainer, tab.dataset.tab)); + }); +}); + +copyBtn.addEventListener('click', () => { + const activeContent = document.querySelector('.curl-command .tab-content.active code'); + navigator.clipboard.writeText(activeContent.innerText); + copyBtn.innerHTML = ''; + lucide.createIcons(); + setTimeout(() => { + copyBtn.innerHTML = ''; + lucide.createIcons(); + }, 2000); + // addLog('Code example copied to clipboard'); +}); + +copyConfigBtn.addEventListener('click', () => { + const activeContent = document.querySelector('.routing-config .tab-content.active code'); + navigator.clipboard.writeText(activeContent.textContent); + copyConfigBtn.innerHTML = ''; + lucide.createIcons(); + setTimeout(() => { + copyConfigBtn.innerHTML = ''; + lucide.createIcons(); + }, 2000); + // addLog('Routing config copied to clipboard'); +}); + +// Modify existing event listeners +providerSelect.addEventListener('change', (e) => { + provider = e.target.value; + updateAllCommands(); + providerDialog.style.display = 'none'; + highlightElement(document.getElementById('providerValue')); + // Find if there are any provider details in localStorage for this provider + let localDetails = localStorage.getItem(`providerDetails-${provider}`); + if(localDetails) { + console.log('Provider details found in localStorage', localDetails); + providerDetails = JSON.parse(localDetails); + updateAllCommands(); + highlightElement(document.getElementById('apiKeyValue')); + } + // addLog(`Provider set to ${provider}`); +}); + +saveApiDetailsBtn.addEventListener('click', () => { + const fields = getProviderFields(provider); + providerDetails = {}; + fields.forEach(field => { + const input = document.getElementById(field.id); + providerDetails[field.id] = input.value; + }); + // Save all provider details in localStorage for this provider + localStorage.setItem(`providerDetails-${provider}`, JSON.stringify(providerDetails)); + updateAllCommands(); + apiKeyDialog.style.display = 'none'; + highlightElement(document.getElementById('apiKeyValue')); +}); + +languageSelect.addEventListener('change', updateRoutingConfig); + +// Initialize +updateAllCommands(); +updateRoutingConfig(); + +// Close dialogs when clicking outside +window.addEventListener('click', (e) => { + if (e.target.classList.contains('dialog-overlay')) { + e.target.style.display = 'none'; + } +}); + +// Close dialogs when hitting escape +window.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + providerDialog.style.display = 'none'; + apiKeyDialog.style.display = 'none'; + logDetailsModal.style.display = 'none'; + } +}); + +// Tab functionality +const tabButtons = document.querySelectorAll('.tab-button'); +const tabContents = document.querySelectorAll('.main-tab-content'); + +function mainTabFocus(tabName) { + if(tabName === 'logs') { + resetLogCounter(); + } + tabButtons.forEach(btn => btn.classList.remove('active')); + tabContents.forEach(content => content.classList.remove('active')); + + document.getElementById(`${tabName}-tab-button`).classList.add('active'); + document.getElementById(`${tabName}-tab`).classList.add('active'); +} + +tabButtons.forEach(button => { + button.addEventListener('click', (e) => { + e.preventDefault(); + let tabName = button.getAttribute('data-tab'); + const href = tabName === 'logs' ? '/public/logs' : '/public/'; + history.pushState(null, '', href); + mainTabFocus(tabName); + }); +}); + +function managePage() { + if(window.location.pathname === '/public/logs') { + mainTabFocus('logs'); + } else { + mainTabFocus('main'); + } +} + +window.addEventListener('popstate', () => { + managePage() +}); + +managePage() + +// Logs functionality +const logsTableBody = document.getElementById('logsTableBody'); +const logDetailsModal = document.getElementById('logDetailsModal'); +const logDetailsContent = document.getElementById('logDetailsContent'); +const closeModal = document.querySelector('.close'); +const clearLogsBtn = document.querySelector('.btn-clear-logs'); + +// SSE for the logs +let logSource; + +function setupLogSource() { + logSource = new EventSource('/log/stream'); + + logSource.addEventListener('connected', (event) => { + console.log('Connected to log stream', event.data); + }); + + logSource.addEventListener('log', (event) => { + const entry = JSON.parse(event.data); + console.log('Received log entry', entry); + addLogEntry(entry.time, entry.method, entry.endpoint, entry.status, entry.duration, entry.requestOptions); + }); + + // Handle heartbeat to keep connection alive + logSource.addEventListener('heartbeat', (event) => { + console.log('Received heartbeat'); + }); + + logSource.onerror = (error) => { + console.error('SSE error (logs):', error); + reconnectLogSource(); + }; +} + +function cleanupLogSource() { + if (logSource) { + console.log('Closing log stream connection'); + logSource.close(); + logSource = null; + } +} + +function reconnectLogSource() { + if (logSource) { + logSource.close(); + } + console.log('Attempting to reconnect to log stream...'); + setTimeout(() => { + setupLogSource(); + }, 5000); // Wait 5 seconds before attempting to reconnect +} + +setupLogSource(); + +function addLogEntry(time, method, endpoint, status, duration, requestOptions) { + const tr = document.createElement('tr'); + tr.classList.add('new-row'); + tr.innerHTML = ` + ${time} + ${method} + ${endpoint} + ${status} + ${duration}ms + + `; + + const viewDetailsBtn = tr.querySelector('.btn-view-details'); + viewDetailsBtn.addEventListener('click', () => showLogDetails(time, method, endpoint, status, duration, requestOptions)); + + if (logsTableBody.children.length > 1) { + logsTableBody.insertBefore(tr, logsTableBody.children[1]); + } else { + logsTableBody.appendChild(tr); + } + + // Ensure the log table does not exceed 100 rows + while (logsTableBody.children.length > 100) { + logsTableBody.removeChild(logsTableBody.lastChild); + } + + // Add a message to the last line of the table + if (logsTableBody.children.length === 100) { + let messageRow = logsTableBody.querySelector('.log-message-row'); + if (!messageRow) { + messageRow = document.createElement('tr'); + messageRow.classList.add('log-message-row'); + const messageCell = document.createElement('td'); + messageCell.colSpan = 6; // Assuming there are 6 columns in the table + messageCell.textContent = 'Only the latest 100 logs are being shown.'; + messageRow.appendChild(messageCell); + logsTableBody.appendChild(messageRow); + } + } + + incrementLogCounter(); + + setTimeout(() => { + tr.className = ''; + }, 500); +} + +function showLogDetails(time, method, endpoint, status, duration, requestOptions) { + logDetailsContent.innerHTML = ` +

Request Details

+

Time: ${time}

+

Method: ${method}

+

Endpoint: ${endpoint}

+

Status: ${status}

+

Duration: ${duration}ms

+

Request:

${JSON.stringify(requestOptions[0].requestParams, null, 2)}

+

Response:

${JSON.stringify(requestOptions[0].response, null, 2)}

+ `; + logDetailsModal.style.display = 'block'; +} + +function incrementLogCounter() { + if(window.location.pathname != '/public/logs') { + logCounter++; + const badge = document.querySelector('header .badge'); + badge.textContent = logCounter; + badge.style.display = 'inline-block'; + } +} + +function resetLogCounter() { + logCounter = 0; + const badge = document.querySelector('header .badge'); + badge.textContent = logCounter; + badge.style.display = 'none'; +} + +closeModal.addEventListener('click', () => { + logDetailsModal.style.display = 'none'; +}); + +window.addEventListener('click', (event) => { + if (event.target === logDetailsModal) { + logDetailsModal.style.display = 'none'; + } +}); + +// Update event listeners for page unload +window.addEventListener('beforeunload', cleanupLogSource); +window.addEventListener('unload', cleanupLogSource); + + +window.onload = function() { + // Run the confetti function only once by storing the state in localStorage + if(!localStorage.getItem('confettiRun')) { + setTimeout(() => { + confetti(); + localStorage.setItem('confettiRun', 'true'); + }, 1000); + } + // confetti({ + // particleCount: 100, + // spread: 70, + // origin: { y: 0.6 } + // }); +}; diff --git a/src/public/snippets.js b/src/public/snippets.js new file mode 100644 index 000000000..ee0316e69 --- /dev/null +++ b/src/public/snippets.js @@ -0,0 +1,230 @@ +const configs = {"nodejs": {}, "python": {}, "curl": {}} + +// Node.js - Simple +configs["nodejs"]["simple"] = ` +// 1. Create config with provider and API key +const config = { + "provider": 'openai', + "api_key": 'Your OpenAI API key', +}; + +// 2. Add this config to the client +const client = new Portkey({config}); + +// 3. Use the client in completion requests +await client.chat.completions.create({ + model: 'gpt-4o', + messages: [{ role: 'user', content: 'Hello, world!' }], +});` + +// Node.js - Load Balancing +configs["nodejs"]["loadBalancing"] = ` +// 1. Create the load-balanced config +const lbConfig = { + "strategy": { "mode": "loadbalance" }, + "targets": [{ + "provider": 'openai', + "api_key": 'Your OpenAI API key', + "weight": 0.7 + },{ + "provider": 'anthropic', + "api_key": 'Your Anthropic API key', + "weight": 0.3, + "override_params": { + "model": 'claude-3-opus-20240229' // Any params you want to override + }, + }], +}; + +// 2. Use the config in completion requests +await client.chat.completions.create({ + model: 'gpt-4o', // The model will be replaced with the one specified in the config + messages: [{ role: 'user', content: 'Hello, world!' }], +}, {config: lbConfig});` + +// Node.js - Fallbacks +configs["nodejs"]["fallbacks"] = ` +// 1. Create the fallback config +const fallbackConfig = { + "strategy": { "mode": "fallback" }, + "targets": [{ // The primary target + "provider": 'openai', + "api_key": 'Your OpenAI API key', + },{ // The fallback target + "provider": 'anthropic', + "api_key": 'Your Anthropic API key', + }], +}; + +// 2. Use the config in completion requests +await client.chat.completions.create({ + model: 'gpt-4o', // The model will be replaced with the one specified in the config + messages: [{ role: 'user', content: 'Hello, world!' }], +}, {config: fallbackConfig});` + +// Node.js - Retries & Timeouts +configs["nodejs"]["autoRetries"] = ` +// 1. Create the retry and timeout config +const retryTimeoutConfig = { + "retry": { + "attempts": 3, + "on_status_codes": [429, 502, 503, 504] // Optional + }, + "request_timeout": 10000, + "provider": 'openai', + "api_key": 'Your OpenAI API key' +}; + +// 2. Use the config in completion requests +await client.chat.completions.create({ + model: 'gpt-4o', // The model will be replaced with the one specified in the config + messages: [{ role: 'user', content: 'Hello, world!' }], +}, {config: retryTimeoutConfig});` + +// Python - Simple +configs["python"]["simple"] = ` +# 1. Create config with provider and API key +config = { + "provider": 'openai', + "api_key": 'Your OpenAI API key', +} + +# 2. Add this config to the client +client = Portkey(config=config) + +# 3. Use the client in completion requests +client.chat.completions.create( + model = 'gpt-4o', + messages = [{ role: 'user', content: 'Hello, world!' }], +)` + +// Python - Load Balancing +configs["python"]["loadBalancing"] = ` +# 1. Create the load-balanced config +lb_config = { + "strategy": { "mode": "loadbalance" }, + "targets": [{ + "provider": 'openai', + "api_key": 'Your OpenAI API key', + "weight": 0.7 + },{ + "provider": 'anthropic', + "api_key": 'Your Anthropic API key', + "weight": 0.3, + "override_params": { + "model": 'claude-3-opus-20240229' # Any params you want to override + }, + }], +} + +# 2. Use the config in completion requests +client.with_options(config=lb_config).chat.completions.create( + model = 'gpt-4o', + messages = [{ role: 'user', content: 'Hello, world!' }], +)` + +// Python - Fallbacks +configs["python"]["fallbacks"] = ` +# 1. Create the fallback config +fallback_config = { + "strategy": { "mode": "fallback" }, + "targets": [{ # The primary target + "provider": 'openai', + "api_key": 'Your OpenAI API key', + },{ # The fallback target + "provider": 'anthropic', + "api_key": 'Your Anthropic API key', + "override_params": { + "model": 'claude-3-opus-20240229' # Any params you want to override + }, + }], +} + +# 2. Use the config in completion requests +client.with_options(config=fallback_config).chat.completions.create( + model = 'gpt-4o', + messages = [{ role: 'user', content: 'Hello, world!' }], +)` + +// Python - Retries & Timeouts +configs["python"]["autoRetries"] = ` +# 1. Create the retry and timeout config +retry_timeout_config = { + "retry": { + "attempts": 3, + "on_status_codes": [429, 502, 503, 504] # Optional + }, + "request_timeout": 10000, + "provider": 'openai', + "api_key": 'Your OpenAI API key' +} + +# 2. Use the config in completion requests +client.with_options(config=retry_timeout_config).chat.completions.create( + model = 'gpt-4o', + messages = [{ role: 'user', content: 'Hello, world!' }], +)` + +// Curl - Simple +configs["curl"]["simple"] = ` +# Store the config in a variable +simple_config='{"provider":"openai","api_key":"Your OpenAI API Key"}' + +# Use the config in completion requests +curl http://localhost:8787/v1/chat/completions \ +\n-H "Content-Type: application/json" \ +\n-H "x-portkey-config: $simple_config" \ +\n-d '{ + "model": "gpt-4o", + "messages": [ + { "role": "user", "content": "Hello!" } + ] +}'` + +// Curl - Load Balancing +configs["curl"]["loadBalancing"] = ` +# Store the config in a variable +lb_config='{"strategy":{"mode":"loadbalance"},"targets":[{"provider":"openai","api_key":"Your OpenAI API key","weight": 0.7 },{"provider":"anthropic","api_key":"Your Anthropic API key","weight": 0.3,"override_params":{"model":"claude-3-opus-20240229"}}]}' + +# Use the config in completion requests +curl http://localhost:8787/v1/chat/completions \ +\n-H "Content-Type: application/json" \ +\n-H "x-portkey-config: $lb_config" \ +\n-d '{ + "model": "gpt-4o", + "messages": [ + { "role": "user", "content": "Hello!" } + ] +}'` + +// Curl - Fallbacks +configs["curl"]["fallbacks"] = ` +# Store the config in a variable +fb_config='{"strategy":{"mode":"fallback"},"targets":[{"provider":"openai","api_key":"Your OpenAI API key"},{"provider":"anthropic","api_key":"Your Anthropic API key","override_params":{"model":"claude-3-opus-20240229"}}]}' + +# Use the config in completion requests +curl http://localhost:8787/v1/chat/completions \ +\n-H "Content-Type: application/json" \ +\n-H "x-portkey-config: $fb_config" \ +\n-d '{ + "model": "gpt-4o", + "messages": [ + { "role": "user", "content": "Hello!" } + ] +}'` + +// Curl - Retries & Timeouts +configs["curl"]["autoRetries"] = ` +# Store the config in a variable +rt_config='{"retry":{"attempts": 3,"on_status_codes": [429, 502, 503, 504]},"request_timeout": 10000, "provider": "openai", "api_key": "Your OpenAI API key"}' + +# Use the config in completion requests +curl http://localhost:8787/v1/chat/completions \ +\n-H "Content-Type: application/json" \ +\n-H "x-portkey-config: $rt_config" \ +\n-d '{ + "model": "gpt-4o", + "messages": [ + { "role": "user", "content": "Hello!" } + ] +}'` \ No newline at end of file diff --git a/src/public/styles/buttons.css b/src/public/styles/buttons.css new file mode 100644 index 000000000..954aa297e --- /dev/null +++ b/src/public/styles/buttons.css @@ -0,0 +1,63 @@ +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.5rem 1rem; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; + background-color: rgb(24, 24, 27); + color: white; + border: 0px; + position: relative; + overflow: hidden; +} + +.btn:hover { + background-color: rgba(24, 24, 27,0.9) +} + +.btn-outline { + border: 1px solid #b8bcc2; + background-color: white; + color: rgb(24, 24, 27); +} + +.btn-outline:hover { + background-color: #f3f4f6; +} + +/* Loading state */ +.btn.loading { + cursor: not-allowed; + opacity: 0.7; +} + +.btn.loading::after { + content: ''; + position: absolute; + width: 1rem; + height: 1rem; + border: 2px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: white; + animation: spin 0.8s linear infinite; +} + +.btn.loading .btn-text { + visibility: hidden; +} + +.btn-outline.loading::after { + border-color: rgba(24, 24, 27, 0.3); + border-top-color: rgb(24, 24, 27); +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/src/public/styles/header.css b/src/public/styles/header.css new file mode 100644 index 000000000..9a49b7b44 --- /dev/null +++ b/src/public/styles/header.css @@ -0,0 +1,103 @@ +/* Header styles */ +header { + background-color: white; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); + padding: 0.75rem 0; + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; +} + +.container { + max-width: 1200px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 1rem; +} + +.logo { + display: flex; + align-items: center; +} + +.logo img { + margin-right: 0.5rem; + max-height: 2rem; +} + +.logo span { + font-size: 0.875rem; + font-weight: normal; + display: flex; + align-items: center; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #22c55e; + margin-left: 8px; + animation: blink 1s infinite; +} + +@keyframes blink { + 0% { opacity: 0; } + 50% { opacity: 1; } + 100% { opacity: 0; } +} + +.header-links { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.header-links a { + color: #2563eb; + text-decoration: none; + font-size: 0.875rem; +} + +.header-links a:hover { + color: #1d4ed8; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .container { + flex-direction: column; + align-items: flex-start; + } + + .logo { + margin-bottom: 0.5rem; + } + + .tabs-container { + margin-bottom: 0.5rem; + } + + .header-links { + width: 100%; + justify-content: space-between; + } +} + +header .badge { + background-color: rgb(239, 68, 68); + color: white; + padding: 0.25rem 0.25rem; + border-radius: 100px; + font-size: 0.65rem; + font-weight: normal; + margin-left: 5px; + min-width: 13px; + /* display: inline-block; */ + text-align: center; + display: none; +} \ No newline at end of file diff --git a/src/public/styles/interative-code.css b/src/public/styles/interative-code.css new file mode 100644 index 000000000..9c0b30b13 --- /dev/null +++ b/src/public/styles/interative-code.css @@ -0,0 +1,178 @@ +pre { + background-color: #f3f4f6; + padding: 0.75rem; + border-radius: 0.375rem; + overflow-x: auto; + font-size: 0.875rem; + position: relative; +} + +.copy-btn { + position: absolute; + top: 0.5rem; + right: 0.5rem; + padding: 0.25rem; + background-color: white; + border: 1px solid #d1d5db; + border-radius: 0.25rem; + cursor: pointer; + z-index: 10; + height: 28px; +} + +.copy-btn svg { + width: 20px; + height: 18px; + color: #393d45; +} + +/* Highlighted values */ +.highlighted-value { + display: inline-block; + position: relative; + cursor: pointer; + transition: transform 0.2s; + padding: 0 0.25rem; + margin: 2px 0; +} + +.highlighted-value.filled { + font-weight: bold; +} + +.highlighted-value:hover { + transform: scale(1.05); +} + +.highlighted-value::before { + content: ''; + position: absolute; + inset: 0; + border-radius: 0.25rem; + transition: all 0.2s; +} + +.highlighted-value.empty::before { + background-color: rgba(252, 165, 165, 0.3); + border: 1px solid rgba(248, 113, 113, 0.5); +} + +.highlighted-value.filled::before { + background-color: rgba(134, 239, 172, 0.2); + border: 1px solid rgba(74, 222, 128, 0.5); +} + +.highlighted-value:hover::before { + opacity: 0.4; +} + +.highlighted-value span { + position: relative; + z-index: 10; +} + +.highlighted-value.empty span { + color: #dc2626; +} + +.highlighted-value.filled span { + color: #16a34a; +} + +@keyframes highlight { + 0% { + background-color: rgba(253, 224, 71, 0.2); + transform: scale(1); + } + 20% { + background-color: rgba(253, 224, 71, 1); + transform: scale(1.05); + } + 100% { + background-color: rgba(253, 224, 71, 0.2); + transform: scale(1); + } +} + +/* Dialog styles */ +.dialog-overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 50; +} + +.dialog { + background-color: white; + border-radius: 0.5rem; + padding: 1.5rem; + width: 90%; + max-width: 500px; +} + +.dialog h3 { + font-size: 1.25rem; + font-weight: bold; + margin-bottom: 0; + margin-top: 0; +} + +.dialog p { + font-size: 0.875rem; + color: #6b7280; + margin-bottom: 1rem; + margin-top: 0; +} + +.select-wrapper { + position: relative; +} + +.select { + width: 100%; + padding: 0.5rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + appearance: none; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); + background-position: right 0.5rem center; + background-repeat: no-repeat; + background-size: 1.5em 1.5em; + font-size: 0.75rem; +} + +.input { + width: 90%; + padding: 0.5rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + margin-bottom: 0.5rem; +} + +.dialog label { + font-size: 0.75rem; + font-weight: bold; + display: inline-block; + padding: 0.25rem; +} + +.dialog .btn { + margin-top: 0.5rem; +} + +.animate-highlight { + animation: highlight 1s ease-out; +} + +.language-select-wrapper { + width: 100px; + display: inline-block; + position: absolute; + z-index: 2; + right: 45px; + top: 0.5rem; + font-size: 12px; +} \ No newline at end of file diff --git a/src/public/styles/logs.css b/src/public/styles/logs.css new file mode 100644 index 000000000..0f7420438 --- /dev/null +++ b/src/public/styles/logs.css @@ -0,0 +1,150 @@ +/* Logs styles */ +.card.logs-card { + margin-top: 1rem; + max-width: 800px; +} + +.logs-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.logs-table-container { + background-color: white; + border-radius: 8px; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); + overflow: hidden; + width: 100%; + max-width: 800px; +} + +.logs-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; +} + +.logs-table th, +.logs-table td { + padding: 0.75rem; + text-align: left; + border-bottom: 1px solid #e5e7eb; + cursor: default; +} + +.logs-table th { + background-color: #f3f4f6; + font-weight: 600; + color: #374151; + text-transform: uppercase; + font-size: 12px; + letter-spacing: 0.05em; +} + +.logs-table tr:hover { + background-color: #f3f4f6; +} + +/* .logs-table td:last-child { + text-align: right; +} */ + +.loading-row { + background-color: #f3f4f6; + color: #6b7280; + font-style: italic; +} + +.loading-row td { + padding: 0.25rem 0.75rem; +} + +.loading-animation { + display: inline-block; + width: 12px; + height: 12px; + border: 2px solid #6b7280; + border-radius: 50%; + border-top-color: transparent; + animation: spin 1s linear infinite; + margin-right: 8px; + vertical-align: middle; +} +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.new-row { + animation: fadeInSlideDown 0.2s ease-out; +} +@keyframes fadeInSlideDown { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.log-time { + font-family: monospace; + font-size: 0.875rem; +} + +.log-method span { + padding: 0.3rem; + background-color: rgba(0, 0, 0, 0.1); + border-radius: 4px; + font-weight: 600; +} + +.log-status span.success { + padding: 0.3rem; + background-color: green; + border-radius: 4px; + color: white; + font-weight: 700; +} + +.log-status span.error { + padding: 0.3rem; + background-color: red; + border-radius: 4px; + color: white; + font-weight: 700; +} + + + +.btn-view-details { + padding: 0.25rem 0.5rem; + background-color: #3b82f6; + color: white; + border: none; + border-radius: 0.25rem; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.btn-view-details:hover { + background-color: #2563eb; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .logs-header { + flex-direction: column; + align-items: stretch; + } + + .logs-search { + width: 100%; + margin-bottom: 1rem; + } +} \ No newline at end of file diff --git a/src/public/styles/modal.css b/src/public/styles/modal.css new file mode 100644 index 000000000..439a4f597 --- /dev/null +++ b/src/public/styles/modal.css @@ -0,0 +1,35 @@ +/* Modal styles */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); +} + +.modal-content { + background-color: white; + margin: 0 0 0 auto; + padding: 2rem; + border-radius: 0rem; + width: 80%; + max-width: 500px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + height: 100vh; + overflow-y: auto; +} + +.close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; + cursor: pointer; +} + +.close:hover { + color: #000; +} \ No newline at end of file diff --git a/src/public/styles/style.css b/src/public/styles/style.css new file mode 100644 index 000000000..cf78ea03f --- /dev/null +++ b/src/public/styles/style.css @@ -0,0 +1,230 @@ +/* Base styles */ +body { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + font-size: 14px; + margin: 0; + padding: 0; + min-height: 100vh; + background-color: #f3f4f6; + color: #213547; + margin-top: 4rem; +} + +a { + color: rgb(24, 24, 27); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.relative { + position: relative; +} + +/* Main content styles */ +.main-content { + max-width: 1200px; + margin: 1rem auto; + padding: 0 1rem; +} + +/* Main content styles */ +.main-content { + max-width: 1200px; + margin: 0 auto; + padding: 1rem; + transition: margin-bottom 0.3s ease; +} + +.left-column { + width: 65%; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.right-column { + width: 35%; + display: flex; + flex-direction: column; +} + +.card { + background-color: white; + border-radius: 0.75rem; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + padding: 1.5rem; + max-width: 600px; + margin: 0rem auto 2rem auto; +} + +.left-column .card { + width: 100%; + max-width: 500px; + margin: 0 auto; +} + +/* Responsive adjustments */ +@media (max-width: 1024px) { + .main-content { + flex-direction: column; + } + + .left-column, + .right-column { + width: 100%; + } +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +h2 { + font-size: 1.125rem; + font-weight: bold; + margin: 0; +} + +.card-subtitle { + font-size: 0.875rem; + color: #6b7280; + margin-bottom: 1rem; +} + +/* Features to Explore Card Styles */ +.features-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + margin-top: 1rem; +} + +.feature-item { + background-color: #f9fafb; + border-radius: 0.5rem; + padding: 1rem; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.feature-item:hover { + box-shadow: 0px 0px 3px 1px rgba(0, 0, 0, 0.1); + cursor: pointer; + transition: all 0.2s; + text-decoration: none; +} + +.feature-item .icon { + width: 2rem; + height: 2rem; + color: #3b82f6; + margin-bottom: 0.5rem; +} + +.feature-item h3 { + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.feature-item p { + font-size: 0.875rem; + color: #6b7280; +} + +/* Next Steps Card Styles */ +.card.next-steps { + margin-top: 1rem; + background-color: transparent; + /* border-top: 1px solid #ccc; */ + padding-top: 2rem; + box-shadow: none; + border-radius: 0; + width: 90%; + max-width: 700px; +} + +.next-steps-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin-top: 1rem; +} + +.next-step-item { + border: 1px solid #babcc0; + border-radius: 0.5rem; + padding: 1rem; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + box-shadow: 0px 5px 3px 2px rgba(0, 0, 0, 0.1); +} + +.next-step-item .icon { + width: 2rem; + height: 2rem; + color: #3b82f6; + margin-bottom: 0.5rem; +} + +.next-step-item h3 { + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.next-step-item p { + font-size: 0.875rem; + color: #6b7280; + margin-bottom: 1rem; +} + +.next-step-item .btn { + margin-top: auto; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .features-grid, + .next-steps-grid { + grid-template-columns: 1fr; + } +} + +#testRequestResponse { + margin-top: 0.5rem; + border-radius: 4px; + font-family: monospace; + /* dark background color */ + background-color: #213547; + color: #f9fafb; + padding: 0.5rem; + display: none; +} + +#testRequestResponse .error { + /* red color that looks good on dark background */ + color: #ff7f7f; +} + +.docs-link { + display: none; + float: right; + margin: 5px 0; +} + +.docs-link a { + color: #3b82f6; +} \ No newline at end of file diff --git a/src/public/styles/tabs.css b/src/public/styles/tabs.css new file mode 100644 index 000000000..951765321 --- /dev/null +++ b/src/public/styles/tabs.css @@ -0,0 +1,73 @@ +/* Tabs styles */ +.tabs-container { + display: flex; + background-color: #f4f4f5; + padding: 0.25rem; + border-radius: 6px; + color: rgb(113, 113, 122); +} + +.tab-button:hover { + color: rgb(9, 9, 11); +} + +.tab-button.active { + color: rgb(9, 9, 11); + /* border-bottom-color: #3b82f6; */ + background-color: white; + font-weight: 500; + box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 1px 2px 0px; +} + +.tabs-container .tab-button { + min-width: 100px; + padding: 0.3rem 0.875rem; +} + +/* Tab content styles */ +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +.main-tab-content { + display: none; +} + +.main-tab-content.active { + display: block; +} + +.tab-button { + padding: 0.5rem 1rem; + background: none; + border: none; + border-bottom: 2px solid transparent; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + color: #6b7280; + transition: all 0.3s ease; + border-radius: 0.375rem; + /* margin-right: 0.5rem; */ +} + +.tabs { + display: flex; + border-bottom: 1px solid #d1d5db; + margin-bottom: 1rem; +} + +.tab { + padding: 0.5rem 1rem; + cursor: pointer; + border-bottom: 2px solid transparent; +} + +.tab.active { + border-bottom-color: #3b82f6; + font-weight: bold; +} \ No newline at end of file diff --git a/src/services/realtimeLlmEventParser.ts b/src/services/realtimeLlmEventParser.ts new file mode 100644 index 000000000..88415cc87 --- /dev/null +++ b/src/services/realtimeLlmEventParser.ts @@ -0,0 +1,160 @@ +import { Context } from 'hono'; + +export class RealtimeLlmEventParser { + private sessionState: any; + + constructor() { + this.sessionState = { + sessionDetails: null, + conversation: { + items: new Map(), + }, + responses: new Map(), + }; + } + + // Main entry point for processing events + handleEvent(c: Context, event: any, sessionOptions: any): void { + switch (event.type) { + case 'session.created': + this.handleSessionCreated(c, event, sessionOptions); + break; + case 'session.updated': + this.handleSessionUpdated(c, event, sessionOptions); + break; + case 'conversation.item.created': + this.handleConversationItemCreated(c, event); + break; + case 'conversation.item.deleted': + this.handleConversationItemDeleted(c, event); + break; + case 'response.done': + this.handleResponseDone(c, event, sessionOptions); + break; + case 'error': + this.handleError(c, event, sessionOptions); + break; + default: + break; + } + } + + // Handle `session.created` event + private handleSessionCreated( + c: Context, + data: any, + sessionOptions: any + ): void { + this.sessionState.sessionDetails = { ...data.session }; + const realtimeEventParser = c.get('realtimeEventParser'); + if (realtimeEventParser) { + c.executionCtx.waitUntil( + realtimeEventParser( + c, + sessionOptions, + {}, + { ...data.session }, + data.type + ) + ); + } + } + + // Handle `session.updated` event + private handleSessionUpdated( + c: Context, + data: any, + sessionOptions: any + ): void { + this.sessionState.sessionDetails = { ...data.session }; + const realtimeEventParser = c.get('realtimeEventParser'); + if (realtimeEventParser) { + c.executionCtx.waitUntil( + realtimeEventParser( + c, + sessionOptions, + {}, + { ...data.session }, + data.type + ) + ); + } + } + + // Conversation-specific handlers + private handleConversationItemCreated(c: Context, data: any): void { + const { item } = data; + this.sessionState.conversation.items.set(item.id, data); + } + + private handleConversationItemDeleted(c: Context, data: any): void { + this.sessionState.conversation.items.delete(data.item_id); + } + + private handleResponseDone(c: Context, data: any, sessionOptions: any): void { + const { response } = data; + this.sessionState.responses.set(response.id, response); + for (const item of response.output) { + const inProgressItem = this.sessionState.conversation.items.get(item.id); + this.sessionState.conversation.items.set(item.id, { + ...inProgressItem, + item, + }); + } + const realtimeEventParser = c.get('realtimeEventParser'); + if (realtimeEventParser) { + const itemSequence = this.rebuildConversationSequence( + this.sessionState.conversation.items + ); + c.executionCtx.waitUntil( + realtimeEventParser( + c, + sessionOptions, + { + conversation: { + items: this.getOrderedConversationItems(itemSequence).slice( + 0, + -1 + ), + }, + }, + data, + data.type + ) + ); + } + } + + private handleError(c: Context, data: any, sessionOptions: any): void { + const realtimeEventParser = c.get('realtimeEventParser'); + if (realtimeEventParser) { + c.executionCtx.waitUntil( + realtimeEventParser(c, sessionOptions, {}, data, data.type) + ); + } + } + + private rebuildConversationSequence(items: Map): string[] { + const orderedItemIds: string[] = []; + + // Find the first item (no previous_item_id) + let currentId: string | undefined = Array.from(items.values()).find( + (data) => data.previous_item_id === null + )?.item?.id; + + // Traverse through the chain using previous_item_id + while (currentId) { + orderedItemIds.push(currentId); + const nextItem = Array.from(items.values()).find( + (data) => data.previous_item_id === currentId + ); + currentId = nextItem?.item?.id; + } + + return orderedItemIds; + } + + private getOrderedConversationItems(sequence: string[]): any { + return sequence.map((id) => this.sessionState.conversation.items.get(id)!); + } +} diff --git a/src/services/transformToProviderRequest.ts b/src/services/transformToProviderRequest.ts index 08da34cf8..af4d56876 100644 --- a/src/services/transformToProviderRequest.ts +++ b/src/services/transformToProviderRequest.ts @@ -1,8 +1,7 @@ import { GatewayError } from '../errors/GatewayError'; -import { MULTIPART_FORM_DATA_ENDPOINTS } from '../globals'; import ProviderConfigs from '../providers'; import { endpointStrings } from '../providers/types'; -import { Options, Params, Targets } from '../types/requestBody'; +import { Params } from '../types/requestBody'; /** * Helper function to set a nested property in an object. @@ -184,10 +183,16 @@ const transformToProviderRequestFormData = ( export const transformToProviderRequest = ( provider: string, params: Params, - inputParams: Params | FormData, + inputParams: Params | FormData | ArrayBuffer, fn: endpointStrings ) => { - if (MULTIPART_FORM_DATA_ENDPOINTS.includes(fn)) return inputParams; + if (inputParams instanceof FormData || inputParams instanceof ArrayBuffer) + return inputParams; + + if (fn === 'proxy') { + return params; + } + const providerAPIConfig = ProviderConfigs[provider].api; if ( providerAPIConfig.transformToFormData && diff --git a/src/start-server.ts b/src/start-server.ts index 8b0934765..a3db483b4 100644 --- a/src/start-server.ts +++ b/src/start-server.ts @@ -1,8 +1,14 @@ #!/usr/bin/env node import { serve } from '@hono/node-server'; +import { serveStatic } from '@hono/node-server/serve-static'; import app from './index'; +import { streamSSE } from 'hono/streaming'; +import { Context } from 'hono'; +import { createNodeWebSocket } from '@hono/node-ws'; +import { realTimeHandlerNode } from './handlers/realtimeHandlerNode'; +import { requestValidator } from './middlewares/requestValidator'; // Extract the port number from the command line arguments const defaultPort = 8787; @@ -10,9 +16,183 @@ const args = process.argv.slice(2); const portArg = args.find((arg) => arg.startsWith('--port=')); const port = portArg ? parseInt(portArg.split('=')[1]) : defaultPort; -serve({ +const isHeadless = args.includes('--headless'); + +// Setup static file serving only if not in headless mode +if ( + !isHeadless && + !( + process.env.NODE_ENV === 'production' || + process.env.ENVIRONMENT === 'production' + ) +) { + const setupStaticServing = async () => { + const { join, dirname } = await import('path'); + const { fileURLToPath } = await import('url'); + const { readFileSync } = await import('fs'); + + const scriptDir = dirname(fileURLToPath(import.meta.url)); + + // Serve the index.html content directly for both routes + const indexPath = join(scriptDir, 'public/index.html'); + const indexContent = readFileSync(indexPath, 'utf-8'); + + const serveIndex = (c: Context) => { + return c.html(indexContent); + }; + + // Set up routes + app.get('/public/logs', serveIndex); + app.get('/public', serveIndex); + app.get('/public/', serveIndex); + + // Serve other static files + app.use( + '/public/*', + serveStatic({ + root: '.', + rewriteRequestPath: (path) => { + return join(scriptDir, path).replace(process.cwd(), ''); + }, + }) + ); + }; + + // Initialize static file serving + await setupStaticServing(); + + /** + * A helper function to enforce a timeout on SSE sends. + * @param fn A function that returns a Promise (e.g. stream.writeSSE()) + * @param timeoutMs The timeout in milliseconds (default: 2000) + */ + async function sendWithTimeout(fn: () => Promise, timeoutMs = 200) { + const timeoutPromise = new Promise((_, reject) => { + const id = setTimeout(() => { + clearTimeout(id); + reject(new Error('Write timeout')); + }, timeoutMs); + }); + + return Promise.race([fn(), timeoutPromise]); + } + + app.get('/log/stream', (c: Context) => { + const clientId = Date.now().toString(); + + // Set headers to prevent caching + c.header('Cache-Control', 'no-cache'); + c.header('X-Accel-Buffering', 'no'); + + return streamSSE(c, async (stream) => { + const addLogClient: any = c.get('addLogClient'); + const removeLogClient: any = c.get('removeLogClient'); + + const client = { + sendLog: (message: any) => + sendWithTimeout(() => stream.writeSSE(message)), + }; + // Add this client to the set of log clients + addLogClient(clientId, client); + + // If the client disconnects (closes the tab, etc.), this signal will be aborted + const onAbort = () => { + removeLogClient(clientId); + }; + c.req.raw.signal.addEventListener('abort', onAbort); + + try { + // Send an initial connection event + await sendWithTimeout(() => + stream.writeSSE({ event: 'connected', data: clientId }) + ); + + // Use an interval instead of a while loop + const heartbeatInterval = setInterval(async () => { + if (c.req.raw.signal.aborted) { + clearInterval(heartbeatInterval); + return; + } + + try { + await sendWithTimeout(() => + stream.writeSSE({ event: 'heartbeat', data: 'pulse' }) + ); + } catch (error) { + // console.error(`Heartbeat failed for client ${clientId}:`, error); + clearInterval(heartbeatInterval); + removeLogClient(clientId); + } + }, 10000); + + // Wait for abort signal + await new Promise((resolve) => { + c.req.raw.signal.addEventListener('abort', () => { + clearInterval(heartbeatInterval); + resolve(undefined); + }); + }); + } catch (error) { + // console.error(`Error in log stream for client ${clientId}:`, error); + } finally { + // Remove this client when the connection is closed + removeLogClient(clientId); + c.req.raw.signal.removeEventListener('abort', onAbort); + } + }); + }); +} + +const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app }); + +app.get( + '/v1/realtime', + requestValidator, + upgradeWebSocket(realTimeHandlerNode) +); + +const server = serve({ fetch: app.fetch, port: port, }); -console.log(`Your AI Gateway is now running on http://localhost:${port} 🚀`); +const url = `http://localhost:${port}`; + +injectWebSocket(server); + +// Loading animation function +async function showLoadingAnimation() { + const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; + let i = 0; + + return new Promise((resolve) => { + const interval = setInterval(() => { + process.stdout.write(`\r${frames[i]} Starting AI Gateway...`); + i = (i + 1) % frames.length; + }, 80); + + // Stop after 1 second + setTimeout(() => { + clearInterval(interval); + process.stdout.write('\r'); + resolve(undefined); + }, 1000); + }); +} + +// Clear the console and show animation before main output +console.clear(); +await showLoadingAnimation(); + +// Main server information with minimal spacing +console.log('\x1b[1m%s\x1b[0m', '🚀 Your AI Gateway is running at:'); +console.log(' ' + '\x1b[1;4;32m%s\x1b[0m', `${url}`); + +// Secondary information on single lines +if (!isHeadless) { + console.log('\n\x1b[90m📱 UI:\x1b[0m \x1b[36m%s\x1b[0m', `${url}/public/`); +} +// console.log('\x1b[90m📚 Docs:\x1b[0m \x1b[36m%s\x1b[0m', 'https://portkey.ai/docs'); + +// Single-line ready message +console.log('\n\x1b[32m✨ Ready for connections!\x1b[0m'); diff --git a/src/types/requestBody.ts b/src/types/requestBody.ts index 87d765c51..272a7b3aa 100644 --- a/src/types/requestBody.ts +++ b/src/types/requestBody.ts @@ -78,7 +78,7 @@ export interface Options { requestTimeout?: number; /** This is used to determine if the request should be transformed to formData Example: Stability V2 */ transformToFormData?: boolean; - /** AWS Bedrock specific */ + /** AWS specific (used for Bedrock and Sagemaker) */ awsSecretAccessKey?: string; awsAccessKeyId?: string; awsSessionToken?: string; @@ -87,6 +87,16 @@ export interface Options { awsRoleArn?: string; awsExternalId?: string; + /** Sagemaker specific */ + amznSagemakerCustomAttributes?: string; + amznSagemakerTargetModel?: string; + amznSagemakerTargetVariant?: string; + amznSagemakerTargetContainerHostname?: string; + amznSagemakerInferenceId?: string; + amznSagemakerEnableExplanations?: string; + amznSagemakerInferenceComponent?: string; + amznSagemakerSessionId?: string; + /** Stability AI specific */ stabilityClientId?: string; stabilityClientUserId?: string; @@ -105,6 +115,7 @@ export interface Options { /** OpenAI specific */ openaiProject?: string; openaiOrganization?: string; + openaiBeta?: string; /** Azure Inference Specific */ azureRegion?: string; @@ -290,7 +301,7 @@ export interface Tool extends AnthropicPromptCache { /** The name of the function. */ type: string; /** A description of the function. */ - function?: Function; + function: Function; } /** diff --git a/start-test.js b/start-test.js index 369be1668..b4ba7beb9 100644 --- a/start-test.js +++ b/start-test.js @@ -2,7 +2,7 @@ import { spawn } from 'node:child_process'; console.log('Starting the application...'); -const app = spawn('node', ['build/start-server.js'], { +const app = spawn('node', ['build/start-server.js', '--headless'], { stdio: 'inherit', });